From f334150b2b11e8a20f5bb8ece080a83517e822a5 Mon Sep 17 00:00:00 2001 From: Neil Williams Date: Wed, 13 Jul 2022 11:39:22 +0200 Subject: Import python-pyepics_3.4.1+ds.orig.tar.xz [dgit import orig python-pyepics_3.4.1+ds.orig.tar.xz] --- INSTALL | 67 + LICENSE | 85 + MANIFEST.in | 12 + PKG-INFO | 35 + README.md | 138 ++ conda-recipe/bld.bat | 2 + conda-recipe/build.sh | 5 + conda-recipe/meta.yaml | 46 + doc/AreaDetector1.png | Bin 0 -> 500375 bytes doc/Makefile | 117 ++ doc/_static/basic_screenshot.png | Bin 0 -> 63939 bytes doc/_static/pyepics.png | Bin 0 -> 22860 bytes doc/_templates/indexsidebar.html | 16 + doc/_templates/layout.html | 28 + doc/advanced.rst | 485 ++++++ doc/alarm.rst | 84 + doc/arrays.rst | 189 +++ doc/autosave.rst | 163 ++ doc/ca.rst | 746 +++++++++ doc/conf.py | 205 +++ doc/devices.rst | 581 +++++++ doc/index.rst | 48 + doc/installation.rst | 181 ++ doc/overview.rst | 554 +++++++ doc/pv.rst | 1010 ++++++++++++ doc/sphinx/theme/epicsdoc/layout.html | 14 + doc/sphinx/theme/epicsdoc/static/basic.css_t | 540 ++++++ doc/sphinx/theme/epicsdoc/static/contents.png | Bin 0 -> 202 bytes doc/sphinx/theme/epicsdoc/static/default.css_t | 310 ++++ doc/sphinx/theme/epicsdoc/static/epicsdoc.css_t | 514 ++++++ doc/sphinx/theme/epicsdoc/static/navigation.png | Bin 0 -> 218 bytes doc/sphinx/theme/epicsdoc/static/sidebar.js | 151 ++ doc/sphinx/theme/epicsdoc/theme.conf | 4 + doc/wx.rst | 426 +++++ doc/wx_motor.png | Bin 0 -> 16703 bytes doc/wx_motor_many.png | Bin 0 -> 33480 bytes doc/wx_motordetail.png | Bin 0 -> 55619 bytes epics/__init__.py | 213 +++ epics/_version.py | 21 + epics/alarm.py | 154 ++ epics/autosave/__init__.py | 16 + epics/autosave/save_restore.py | 206 +++ epics/ca.py | 2016 +++++++++++++++++++++++ epics/compat/CaChannel.py | 539 ++++++ epics/compat/__init__.py | 5 + epics/compat/ca_util.py | 659 ++++++++ epics/compat/epicsPV.py | 134 ++ epics/dbr.py | 385 +++++ epics/device.py | 326 ++++ epics/devices/__init__.py | 23 + epics/devices/ad_base.py | 40 + epics/devices/ad_fileplugin.py | 94 ++ epics/devices/ad_image.py | 31 + epics/devices/ad_mca.py | 293 ++++ epics/devices/ad_overlay.py | 37 + epics/devices/ad_perkinelmer.py | 189 +++ epics/devices/ai.py | 16 + epics/devices/ao.py | 16 + epics/devices/bi.py | 17 + epics/devices/bo.py | 15 + epics/devices/mca.py | 607 +++++++ epics/devices/ordereddict.py | 125 ++ epics/devices/scaler.py | 71 + epics/devices/scan.py | 342 ++++ epics/devices/srs570.py | 91 + epics/devices/struck.py | 164 ++ epics/devices/transform.py | 92 ++ epics/devices/xspress3.py | 313 ++++ epics/motor.py | 643 ++++++++ epics/multiproc.py | 49 + epics/pv.py | 1129 +++++++++++++ epics/qt/pvprobe_qt.py | 85 + epics/utils.py | 74 + epics/utils2.py | 28 + epics/utils3.py | 53 + epics/wx/__init__.py | 44 + epics/wx/motordetailframe.py | 429 +++++ epics/wx/motorpanel.py | 312 ++++ epics/wx/ogllib.py | 110 ++ epics/wx/ordereddict.py | 125 ++ epics/wx/utils.py | 513 ++++++ epics/wx/wxlib.py | 1226 ++++++++++++++ scripts/Motor_Display.py | 97 ++ scripts/README | 30 + scripts/caget.py | 73 + scripts/mpanel.py | 321 ++++ scripts/save_restore.py | 71 + scripts/wxProbe.py | 118 ++ setup.cfg | 11 + setup.py | 83 + tests/AutoSaveSimple.req | 10 + tests/AutoSaveTest.req | 2 + tests/Setup/pydebug.db | 274 +++ tests/Setup/simulator.py | 150 ++ tests/Setup/st.cmd | 26 + tests/alarm.py | 35 + tests/autosave_test.py | 12 + tests/ca_connection_callback.py | 21 + tests/ca_fastconn.py | 71 + tests/ca_subscribe.py | 20 + tests/ca_subscribe2.py | 28 + tests/ca_type_conversion.py | 76 + tests/ca_unittest.py | 451 +++++ tests/caget_large_arrays_slow_net.py | 35 + tests/cathread_tests.py | 110 ++ tests/connect.py | 68 + tests/debugtime.py | 44 + tests/device_ao.py | 32 + tests/func_camonitor.py | 32 + tests/m.py | 5 + tests/memleak.py | 58 + tests/memleak_put.py | 57 + tests/memory_motor.py | 51 + tests/motor_simple.py | 34 + tests/no_monitor.py | 17 + tests/o.py | 44 + tests/o1.py | 10 + tests/putwait.py | 37 + tests/pv_callback.py | 26 + tests/pv_connection_callback.py | 27 + tests/pv_disconnect_with_getcb.py | 51 + tests/pv_initial_callbacks.py | 49 + tests/pv_multiple_callbacks.py | 37 + tests/pv_subarray_test.py | 43 + tests/pv_type_conversion.py | 73 + tests/pv_unittest.py | 626 +++++++ tests/pvnames.py | 72 + tests/sg_test.py | 43 + tests/test_cas.py | 180 ++ tests/test_install.py | 46 + tests/test_multiprocessing.py | 44 + tests/test_pool.py | 51 + tests/test_threading.py | 71 + tests/thread_put.py | 44 + tests/thread_put2.py | 28 + tests/thread_test.py | 44 + tests/thread_test_BNLt2.py | 43 + tests/thread_test_BNLtt.py | 60 + versioneer.py | 1822 ++++++++++++++++++++ 139 files changed, 24920 insertions(+) create mode 100644 INSTALL create mode 100644 LICENSE create mode 100644 MANIFEST.in create mode 100644 PKG-INFO create mode 100644 README.md create mode 100644 conda-recipe/bld.bat create mode 100644 conda-recipe/build.sh create mode 100644 conda-recipe/meta.yaml create mode 100644 doc/AreaDetector1.png create mode 100644 doc/Makefile create mode 100644 doc/_static/basic_screenshot.png create mode 100644 doc/_static/pyepics.png create mode 100644 doc/_templates/indexsidebar.html create mode 100644 doc/_templates/layout.html create mode 100644 doc/advanced.rst create mode 100644 doc/alarm.rst create mode 100644 doc/arrays.rst create mode 100644 doc/autosave.rst create mode 100644 doc/ca.rst create mode 100644 doc/conf.py create mode 100644 doc/devices.rst create mode 100644 doc/index.rst create mode 100644 doc/installation.rst create mode 100644 doc/overview.rst create mode 100644 doc/pv.rst create mode 100644 doc/sphinx/theme/epicsdoc/layout.html create mode 100644 doc/sphinx/theme/epicsdoc/static/basic.css_t create mode 100644 doc/sphinx/theme/epicsdoc/static/contents.png create mode 100644 doc/sphinx/theme/epicsdoc/static/default.css_t create mode 100644 doc/sphinx/theme/epicsdoc/static/epicsdoc.css_t create mode 100644 doc/sphinx/theme/epicsdoc/static/navigation.png create mode 100644 doc/sphinx/theme/epicsdoc/static/sidebar.js create mode 100644 doc/sphinx/theme/epicsdoc/theme.conf create mode 100644 doc/wx.rst create mode 100644 doc/wx_motor.png create mode 100644 doc/wx_motor_many.png create mode 100644 doc/wx_motordetail.png create mode 100644 epics/__init__.py create mode 100644 epics/_version.py create mode 100644 epics/alarm.py create mode 100644 epics/autosave/__init__.py create mode 100755 epics/autosave/save_restore.py create mode 100755 epics/ca.py create mode 100644 epics/compat/CaChannel.py create mode 100644 epics/compat/__init__.py create mode 100644 epics/compat/ca_util.py create mode 100755 epics/compat/epicsPV.py create mode 100644 epics/dbr.py create mode 100644 epics/device.py create mode 100644 epics/devices/__init__.py create mode 100644 epics/devices/ad_base.py create mode 100644 epics/devices/ad_fileplugin.py create mode 100644 epics/devices/ad_image.py create mode 100644 epics/devices/ad_mca.py create mode 100644 epics/devices/ad_overlay.py create mode 100644 epics/devices/ad_perkinelmer.py create mode 100644 epics/devices/ai.py create mode 100644 epics/devices/ao.py create mode 100644 epics/devices/bi.py create mode 100644 epics/devices/bo.py create mode 100644 epics/devices/mca.py create mode 100644 epics/devices/ordereddict.py create mode 100755 epics/devices/scaler.py create mode 100644 epics/devices/scan.py create mode 100755 epics/devices/srs570.py create mode 100755 epics/devices/struck.py create mode 100644 epics/devices/transform.py create mode 100755 epics/devices/xspress3.py create mode 100644 epics/motor.py create mode 100644 epics/multiproc.py create mode 100755 epics/pv.py create mode 100644 epics/qt/pvprobe_qt.py create mode 100644 epics/utils.py create mode 100644 epics/utils2.py create mode 100644 epics/utils3.py create mode 100755 epics/wx/__init__.py create mode 100755 epics/wx/motordetailframe.py create mode 100755 epics/wx/motorpanel.py create mode 100644 epics/wx/ogllib.py create mode 100644 epics/wx/ordereddict.py create mode 100755 epics/wx/utils.py create mode 100644 epics/wx/wxlib.py create mode 100755 scripts/Motor_Display.py create mode 100644 scripts/README create mode 100644 scripts/caget.py create mode 100644 scripts/mpanel.py create mode 100755 scripts/save_restore.py create mode 100755 scripts/wxProbe.py create mode 100644 setup.cfg create mode 100644 setup.py create mode 100644 tests/AutoSaveSimple.req create mode 100644 tests/AutoSaveTest.req create mode 100644 tests/Setup/pydebug.db create mode 100644 tests/Setup/simulator.py create mode 100755 tests/Setup/st.cmd create mode 100644 tests/alarm.py create mode 100644 tests/autosave_test.py create mode 100644 tests/ca_connection_callback.py create mode 100644 tests/ca_fastconn.py create mode 100644 tests/ca_subscribe.py create mode 100644 tests/ca_subscribe2.py create mode 100644 tests/ca_type_conversion.py create mode 100644 tests/ca_unittest.py create mode 100644 tests/caget_large_arrays_slow_net.py create mode 100644 tests/cathread_tests.py create mode 100644 tests/connect.py create mode 100755 tests/debugtime.py create mode 100644 tests/device_ao.py create mode 100644 tests/func_camonitor.py create mode 100644 tests/m.py create mode 100644 tests/memleak.py create mode 100644 tests/memleak_put.py create mode 100644 tests/memory_motor.py create mode 100644 tests/motor_simple.py create mode 100644 tests/no_monitor.py create mode 100644 tests/o.py create mode 100644 tests/o1.py create mode 100644 tests/putwait.py create mode 100644 tests/pv_callback.py create mode 100644 tests/pv_connection_callback.py create mode 100644 tests/pv_disconnect_with_getcb.py create mode 100644 tests/pv_initial_callbacks.py create mode 100644 tests/pv_multiple_callbacks.py create mode 100644 tests/pv_subarray_test.py create mode 100644 tests/pv_type_conversion.py create mode 100755 tests/pv_unittest.py create mode 100644 tests/pvnames.py create mode 100644 tests/sg_test.py create mode 100644 tests/test_cas.py create mode 100644 tests/test_install.py create mode 100644 tests/test_multiprocessing.py create mode 100644 tests/test_pool.py create mode 100644 tests/test_threading.py create mode 100644 tests/thread_put.py create mode 100644 tests/thread_put2.py create mode 100644 tests/thread_test.py create mode 100644 tests/thread_test_BNLt2.py create mode 100644 tests/thread_test_BNLtt.py create mode 100644 versioneer.py diff --git a/INSTALL b/INSTALL new file mode 100644 index 0000000..593be07 --- /dev/null +++ b/INSTALL @@ -0,0 +1,67 @@ +xInstallation instructions for Py-Epics3 +======================================= + +To install the epics module from source, use + + python setup.py install + +Or, + + pip install . + +This assumes that Python 2.5 or higher is installed and is the +enviroment you wish to istall pyepics in to. + +By default the object libraries for Epics Channel Access (libCom.so +and libca.so for linux, libCom.dylib and libca.dylib for Mac OS X and +Com.dll and ca.dll for windows) are included with this distribution +and installed for you. To prevent the installation of the binaries use + + NOLIBCA=1 python setup.py install + +The code is tested via continuation integration on linux for +python 2.7, 3.5, and 3.6. It is also extensively used on +on windows and OSX machine. + + +Locating CA shared libraries +============================ + +On Unix systems, the shared libraries for Epics Channel Access (libca.so, +libCom.so) must be found by Python at runtime. Since Epics installations +generally leaves these in an architecture-specific location (and not +"installed" into normal system-wide library directories), you will probably +need to set this up once per machine. There are a few ways to do this: + + 1. set the environmental variable LD_LIBRARY_PATH (or DYLD_LIBRARY_PATH) + to point to the directory with the shared object libraries: + + export LD_LIBRARY_PATH=/usr/local/epics/base/lib/linux-x86 + + setenv LD_LIBRARY_PATH /usr/local/epics/base/lib/linux-x86 + + 2. set the PATH environmental variable to make sure that the location of + libca is in the PATH. + +Both options may need to be set for each user shell that uses CA, and so +should probably be put in a shell startup script. + +Testing the Location of CA shared library +========================================== + +The setup.py script will test where libca might be found and print a +prominent warning if libca.so cannot be found. + +To test this yourself, run python from the current directory and +type: + >>> import lib as epics + >>> epics.ca.find_libca() + +This should report the full name of dynamic CA library (libca.so, ca.dll, +or libca.dylib). It it does not, or reports an error, you should +locate libca.so and set PATH or LD_LIBRARY_PATH to include the path +containing this file. + + +Matt Newville +Last Update: 2017-05-01 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..04b9a63 --- /dev/null +++ b/LICENSE @@ -0,0 +1,85 @@ +The epics python module was orignally written by + + Matthew Newville + CARS, University of Chicago + +There have been several contributions from many others, notably Angus +Gratton . See the Acknowledgements section of +the documentation for a list of more contributors. + +Except where explicitly noted, all files in this distribution are licensed +under the Epics Open License.: + +------------------------------------------------ + +Copyright 2010 Matthew Newville, The University of Chicago. All rights reserved. + +The epics python module is distributed subject to the following license conditions: +SOFTWARE LICENSE AGREEMENT +Software: epics python module + + 1. The "Software", below, refers to the epics python module (in either + source code, or binary form and accompanying documentation). Each + licensee is addressed as "you" or "Licensee." + + 2. The copyright holders shown above and their third-party licensors + hereby grant Licensee a royalty-free nonexclusive license, subject to + the limitations stated herein and U.S. Government license rights. + + 3. You may modify and make a copy or copies of the Software for use + within your organization, if you meet the following conditions: + + 1. Copies in source code must include the copyright notice and this + Software License Agreement. + + 2. Copies in binary form must include the copyright notice and this + Software License Agreement in the documentation and/or other + materials provided with the copy. + + 4. You may modify a copy or copies of the Software or any portion of + it, thus forming a work based on the Software, and distribute copies of + such work outside your organization, if you meet all of the following + conditions: + + 1. Copies in source code must include the copyright notice and this + Software License Agreement; + + 2. Copies in binary form must include the copyright notice and this + Software License Agreement in the documentation and/or other + materials provided with the copy; + + 3. Modified copies and works based on the Software must carry + prominent notices stating that you changed specified portions of + the Software. + + 5. Portions of the Software resulted from work developed under a + U.S. Government contract and are subject to the following license: the + Government is granted for itself and others acting on its behalf a + paid-up, nonexclusive, irrevocable worldwide license in this computer + software to reproduce, prepare derivative works, and perform publicly + and display publicly. + + 6. WARRANTY DISCLAIMER. THE SOFTWARE IS SUPPLIED "AS IS" WITHOUT + WARRANTY OF ANY KIND. THE COPYRIGHT HOLDERS, THEIR THIRD PARTY + LICENSORS, THE UNITED STATES, THE UNITED STATES DEPARTMENT OF ENERGY, + AND THEIR EMPLOYEES: (1) DISCLAIM ANY WARRANTIES, EXPRESS OR IMPLIED, + INCLUDING BUT NOT LIMITED TO ANY IMPLIED WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE, TITLE OR NON-INFRINGEMENT, (2) DO NOT + ASSUME ANY LEGAL LIABILITY OR RESPONSIBILITY FOR THE ACCURACY, + COMPLETENESS, OR USEFULNESS OF THE SOFTWARE, (3) DO NOT REPRESENT THAT + USE OF THE SOFTWARE WOULD NOT INFRINGE PRIVATELY OWNED RIGHTS, (4) DO + NOT WARRANT THAT THE SOFTWARE WILL FUNCTION UNINTERRUPTED, THAT IT IS + ERROR-FREE OR THAT ANY ERRORS WILL BE CORRECTED. + + 7. LIMITATION OF LIABILITY. IN NO EVENT WILL THE COPYRIGHT HOLDERS, + THEIR THIRD PARTY LICENSORS, THE UNITED STATES, THE UNITED STATES + DEPARTMENT OF ENERGY, OR THEIR EMPLOYEES: BE LIABLE FOR ANY INDIRECT, + INCIDENTAL, CONSEQUENTIAL, SPECIAL OR PUNITIVE DAMAGES OF ANY KIND OR + NATURE, INCLUDING BUT NOT LIMITED TO LOSS OF PROFITS OR LOSS OF DATA, + FOR ANY REASON WHATSOEVER, WHETHER SUCH LIABILITY IS ASSERTED ON THE + BASIS OF CONTRACT, TORT (INCLUDING NEGLIGENCE OR STRICT LIABILITY), OR + OTHERWISE, EVEN IF ANY OF SAID PARTIES HAS BEEN WARNED OF THE + POSSIBILITY OF SUCH LOSS OR DAMAGES. + +------------------------------------------------ + diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..ef7b493 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,12 @@ +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 * +recursive-include tests *.py *.db *.cmd *.req +recursive-include doc * +recursive-include dlls * +recursive-include conda-recipe * +recursive-exclude doc/_build * +recursive-exclude doc *.pdf +recursive-exclude tests/Misc * + diff --git a/PKG-INFO b/PKG-INFO new file mode 100644 index 0000000..c1987c7 --- /dev/null +++ b/PKG-INFO @@ -0,0 +1,35 @@ +Metadata-Version: 1.1 +Name: pyepics +Version: 3.4.1 +Summary: Epics Channel Access for Python +Home-page: http://pyepics.github.io/pyepics/ +Author: Matthew Newville +Author-email: newville@cars.uchicago.edu +License: Epics Open License +Download-URL: http://pyepics.github.io/pyepics/ +Description: 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. + + In addition, the library includes convenience classes to define + 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. + +Platform: Windows +Platform: Linux +Platform: Mac OS X +Classifier: Intended Audience :: Science/Research +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Topic :: Scientific/Engineering diff --git a/README.md b/README.md new file mode 100644 index 0000000..f263850 --- /dev/null +++ b/README.md @@ -0,0 +1,138 @@ +[![Travis CI](https://travis-ci.org/pyepics/pyepics.png)](https://travis-ci.org/pyepics/pyepics) [![Zenondo](https://zenodo.org/badge/4185/pyepics/pyepics.svg)](https://zenodo.org/badge/latestdoi/4185/pyepics/pyepics) + +# PyEpics 3: Epics Channel Access for Python + + +PyEpics is a Python interface to the EPICS Channel Access (CA) library +for the EPICS control system. + +The PyEpics module includes both low-level (C-like) and higher-level access +(with Python objects) to the EPICS Channnel Access (CA) protocol. Python's +ctypes library is used to wrap the basic CA functionality, with higher +level objects on top of that basic interface. This approach has several +advantages including no need for extension code written in C, better +thread-safety, and easier installation on multiple platforms. + +## Installation + +This package requires python2.6 or higher. The EPICS Channel Access +library v 3.14.8 or higher is also required, with v 3.14.12 or higher being +recommended. Specifically, the shared libraries libCom.so and libca.so +(or Com.dll and ca.dll on Windows, or libca.dylib and libCom.dylib on macOS) +are required to use this module. + +To support this requirement, suitably recent versions of the libraries are +included here (version 3.15.3), and the OS-appropriate library will be +installed alongside the python packages. To install from source: + +``` +> python setup.py install +``` + +Or, + +``` +> pip install . +``` + +If it is desirable to forgo installation of the pre-packaged EPICS libraries, +(i.e. suitable libraries already exist on the target system), then simply +define the `NOLIBCA` environment variable prior to installation: + +``` +> NOLIBCA=1 python setup.py install +``` + +Or, + +``` +> NOLIBCA=1 pip install . +``` + +For additional installation details, see the INSTALL file. Binary installers +for Windows are available. + +## License + +This code is distributed under the Epics Open License + +## Overview + +Py-Epics3 provides two principle modules: ca, and pv, and functions +caget(), caput(), and cainfo() for the simplest of interaction with EPICS. +In addition, there are modules for Epics Motors and Alarms, autosave support +via CA, and special widget classes for using EPICS PVs with wxPython. + + +## caget(), caput() and cainfo() + +The simplest interface to EPICS Channel Access provides functions caget(), +caput(), and cainfo(), similar to the EZCA interface and to the +EPICS-supplied command line utilities. These all take the name of an Epics +Process Variable as the first argument. + +```python +>>> from epics import caget, caput, cainfo +>>> print caget('XXX:m1.VAL') +1.200 +>>> caput('XXX:m1.VAL',2.30) +1 +>>> cainfo('XXX.m1.VAL') +== XXX:m1.VAL (double) == + value = 2.3 + char_value = 2.3000 + count = 1 + units = mm + precision = 4 + host = xxx.aps.anl.gov:5064 + access = read/write + status = 1 + severity = 0 + timestamp = 1265996455.417 (2010-Feb-12 11:40:55.417) + upper_ctrl_limit = 200.0 + lower_ctrl_limit = -200.0 + upper_disp_limit = 200.0 + lower_disp_limit = -200.0 + upper_alarm_limit = 0.0 + lower_alarm_limit = 0.0 + upper_warning_limit = 0.0 + lower_warning = 0.0 + PV is monitored internally + no user callbacks defined. +============================= +``` + +## PV: Object Oriented CA interface + +The general concept is that an Epics Process Variable is implemented as a +Python PV object, which provides a natural way to interact with EPICS. + +```python +>>> import epics + +>>> pv = epics.PV('PVName') +>>> pv.connected +True +>>> pv.get() +3.14 +>>> pv.put(2.71) +``` + +Channel Access features that are included here: + +* user callbacks - user-supplied Python function(s) that are run when a PV's + value, access rights, or connection status changes +* control values - a full Control DBR record can be requested +* enumeration strings - enum PV types have integer or string representation, + and you get access to both +* put with wait - The PV.put() method can optionally wait until the record is + done processing (with timeout) + +Features that you won't have to worry about: + +* connection management (unless you choose to worry about this) +* PV record types - this is handled automatically. + + +Matt Newville +Last Update: 18-Apr-2016 diff --git a/conda-recipe/bld.bat b/conda-recipe/bld.bat new file mode 100644 index 0000000..c40a9bb --- /dev/null +++ b/conda-recipe/bld.bat @@ -0,0 +1,2 @@ +"%PYTHON%" setup.py install +if errorlevel 1 exit 1 diff --git a/conda-recipe/build.sh b/conda-recipe/build.sh new file mode 100644 index 0000000..c57e6fb --- /dev/null +++ b/conda-recipe/build.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +$PYTHON setup.py install + +# Add more build steps here, if they are necessary. diff --git a/conda-recipe/meta.yaml b/conda-recipe/meta.yaml new file mode 100644 index 0000000..8bdd3c0 --- /dev/null +++ b/conda-recipe/meta.yaml @@ -0,0 +1,46 @@ +package: + name: pyepics + version: "3.2.5rc3" + +source: + git_rev: 3.2.5rc3 + git_url: https://github.com/pyepics/pyepics.git + +requirements: + build: + - python + - setuptools + + run: + - python + - numpy + - pyparsing + +test: + # Python imports + imports: + - epics + - epics.autosave + - epics.compat + - epics.devices + + # commands: + # You can put test commands to be run here. Use this to test that the + # entry points work. + + + # You can also put a file called run_test.py in the recipe that will be run + # at test time. + + # requires: + # Put any additional test requirements here. For example + # - nose + +about: + home: http://pyepics.github.io/pyepics/ + license: Epics Open License + summary: 'Epics Channel Access for Python' + +# See +# http://docs.continuum.io/conda/build.html for +# more information about meta.yaml diff --git a/doc/AreaDetector1.png b/doc/AreaDetector1.png new file mode 100644 index 0000000..272c105 Binary files /dev/null and b/doc/AreaDetector1.png differ diff --git a/doc/Makefile b/doc/Makefile new file mode 100644 index 0000000..760521c --- /dev/null +++ b/doc/Makefile @@ -0,0 +1,117 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +INSTALLDIR = /home/newville/public_html/Epics/Python/pyepics3 +INSTALLDIR = /www/apache/htdocs/software/python/pyepics3 + + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest latexpdf +.PHONY: all install pdf + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The EPUB pages are in $(BUILDDIR)/epub." + +pdf: latex + cd $(BUILDDIR)/latex && pdflatex epics.tex + cd $(BUILDDIR)/latex && makeindex -s python.ist epics.idx + cd $(BUILDDIR)/latex && pdflatex epics.tex + cp -pr $(BUILDDIR)/latex/epics.pdf $(BUILDDIR)/html/pyepics.pdf + +all: html pdf + +install: all + cp -pr $(BUILDDIR)/latex/epics.pdf $(INSTALLDIR)/pyepics.pdf + cp -pr $(BUILDDIR)/html/* $(INSTALLDIR)/. + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + -rm -rf $(BUILDDIR)/* + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/epics.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/epics.qhc" + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) _build/latex + @echo + @echo "Build finished; the LaTeX files are in _build/latex." + @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ + "run these through (pdf)latex." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) _build/latex + @echo "Running LaTeX files through pdflatex..." + make -C _build/latex all-pdf + @echo "pdflatex finished; the PDF files are in _build/latex." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." diff --git a/doc/_static/basic_screenshot.png b/doc/_static/basic_screenshot.png new file mode 100644 index 0000000..567dbe3 Binary files /dev/null and b/doc/_static/basic_screenshot.png differ diff --git a/doc/_static/pyepics.png b/doc/_static/pyepics.png new file mode 100644 index 0000000..270dc84 Binary files /dev/null and b/doc/_static/pyepics.png differ diff --git a/doc/_templates/indexsidebar.html b/doc/_templates/indexsidebar.html new file mode 100644 index 0000000..11da02f --- /dev/null +++ b/doc/_templates/indexsidebar.html @@ -0,0 +1,16 @@ +

Download

+

Current version: {{ release }}

+

Download: +

+

+ +Development version:
    github.com
+ +

Documentation

+ + PDF Format +

diff --git a/doc/_templates/layout.html b/doc/_templates/layout.html new file mode 100644 index 0000000..5a1c9c2 --- /dev/null +++ b/doc/_templates/layout.html @@ -0,0 +1,28 @@ +{% extends "!layout.html" %} +{% block rootrellink %} +

  • [install
  • +
  • |overview
  • +
  • |pv
  • +
  • |ca
  • +
  • |arrays
  • +
  • |devices
  • +
  • |alarm
  • +
  • |autosave
  • +
  • |wx
  • +
  • |advanced]
  • +{% endblock %} + +{% block relbar1 %} + +{{ super() }} +{% endblock %} + + +{# put the sidebar before the body #} +{% block sidebar1 %}{{ sidebar() }}{% endblock %} +{% block sidebar2 %}{% endblock %} diff --git a/doc/advanced.rst b/doc/advanced.rst new file mode 100644 index 0000000..90252e8 --- /dev/null +++ b/doc/advanced.rst @@ -0,0 +1,485 @@ +=============================================== +Advanced Topic with Python Channel Access +=============================================== + +This chapter contains a variety of "usage notes" and implementation +details that may help in getting the best performance from the +pyepics module. + + +.. _advanced-get-timeouts-label: + + +The wait and timeout options for get(), ca.get_complete() +============================================================== + +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:`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 +timeout value. If the value returned is ``None``, then either the PV has +truly disconnected or the timeout passed before receiving the value. If +the *get* is incomplete, in that the PV is connected but the data has +simply not been received yet, a subsequent :func:`epics.caget` or +:func:`pv.get` will eventually complete and receive the value. That is, if +a PV for a large waveform record reports that it is connected, but a +:func:`pv.get` returns None, simply trying again later will probably work:: + + >>> p = epics.PV('LargeWaveform') + >>> val = p.get() + >>> val + >>> time.sleep(10) + >>> val = p.get() + + +At the lowest level (which :func:`pv.get` and :func:`epics.caget` use), +: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:`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:`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:`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:`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:`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:`epics.ca.get` or :func:`epics.ca.get_complete`. + +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) + +Again, that's the maximum time that will be waited, and if the data is +received faster than that, the *get* will return as soon as it can. + + +.. _advanced-connecting-many-label: + +Strategies for connecting to a large number of PVs +==================================================== + +Occasionally, you may find that you need to quickly connect to a large +number of PVs, say to write values to disk. The most straightforward way +to do this, say:: + + import epics + + pvnamelist = read_list_pvs() + pv_vals = {} + for name in pvnamelist: + pv = epics.PV(name) + pv_vals[name] = pv.get() + +or even just:: + + values = [epics.caget(name) for name in pvnamelist] + + +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 + pvchids.append(chid) + + # connect + for chid in pvchids: + ca.connect_channel(chid) + + # request get, but do not wait for result + ca.poll() + for chid in pvchids: + ca.get(chid, wait=False) # note 2 + + # now wait for get() to complete + ca.poll() + for chid in pvchids: + val = ca.get_complete(data[0]) + 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: + +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:`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:`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 +in order for events to be processed. The simplest way to do this is with +:meth:`time.sleep`, so that an event loop can simply be:: + + >>> while True: + >>> time.sleep(0.001) + +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:`epics.ca.pend_event` or +:meth:`epics.ca.poll` to be run, better performance may be achieved with an event +loop of:: + + >>> while True: + >>> epics.poll(evt=1.e-5, iot=0.1) + +as the loop will be run more often than using :meth:`time.sleep`. + + +.. index:: Threads +.. _advanced-threads-label: + + +Using Python Threads +========================= + +An important feature of the PyEpics package is that it can be used with +Python threads, as Epics 3.14 supports threads for client code. Even in +the best of cases, working with threads can be somewhat tricky and lead to +unexpected behavior, and the Channel Access library adds a small level of +complication for using CA with Python threads. The result is that some +precautions may be in order when using PyEpics and threads. This section +discusses the strategies for using threads with PyEpics. + +First, to use threads with Channel Access, you must have +:data:`epics.ca.PREEMPTIVE_CALLBACK` = ``True``. This is the default +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:`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. + +Channel Access Contexts +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +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:`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:`epics.ca.create_context` +(and then, being a good citizen, destroy this context as the thread ends +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 +(probably from the main thread). This avoids many potential pitfalls (and +crashes), and can be done fairly simply. It is the default mode when using +PV objects. + +The most explicit use of contexts is to put :func:`epics.ca.create_context` +at the start of each function call as a thread target, and +:func:`epics.ca.destroy_context` at the end of each thread. This will +cause all the activity in that thread to be done in its own context. This +works, but means more care is needed, and so is not the recommended. + + +The best way to attach to the initially created context is to call +:meth:`epics.ca.use_initial_context` before any other CA calls in each +function that will be called by :meth:`Thread.run`. Equivalently, you can +add a :func:`withInitialContext` decorator to the function. Creating a PV +object will implicitly do this for you, as long as it is your first CA +action in the function. Each time you do a :meth:`PV.get` or +:meth:`PV.put` (or a few other methods), it will also check that the initial +context is being used. + +Of course, this approach requires CA to be initialized already. Doing that +*in the main thread* is highly recommended. If it happens in a child +thread, that thread must exist for all CA work, so either the life of the +process or with great care for processes that do only some CA calls. If +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:`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 +explicitly set the context, but still ensures that the initial context is +used in all functions. + +How to work with CA and Threads +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Summarizing the discussion above, to use threads you must use run in +PREEMPTIVE_CALLBACK mode. Furthermore, it is recommended that you use a +single context, and that you initialize CA in the main program thread so +that your single CA context belongs to the main thread. Using PV objects +exclusively makes this easy, but it can also be accomplished relatively +easily using the lower-level ca interface. The options for using threads +(in approximate order of reliability) are then: + + 1. use PV objects for threading work. This ensures you're working in a + single CA context. + + 2. use :class:`CAThread` instead of :class:`Thread` for threads that + will use CA calls. + + 3. put :func:`epics.ca.use_initial_context` at the top of all + functions that might be a Thread target function, or decorate them with + :func:`withInitialContext` decorator, *@withInitialContext*. + + 4. use :func:`epics.ca.create_context` at the top of all functions + that are inside a new thread, and be sure to put + :func:`epics.ca.destroy_context` at the end of the function. + + 5. ignore this advise and hope for the best. If you're not creating + new PVs and only reading values of PVs created in the main thread + inside a child thread, you may not see a problems, at least not until + you try to do something fancier. + + +Thread Examples +~~~~~~~~~~~~~~~ + +This is a simplified version of test code using Python threads. It is +based on code originally from Friedrich Schotte, NIH, and included as +`thread_test.py` in the `tests` directory of the source distribution. + +In this example, we define a `run_test` procedure which will create PVs +from a supplied list, and monitor these PVs, printing out the values when +they change. Two threads are created and run concurrently, with +overlapping PV lists, though one thread is run for a shorter time than the +other. + +.. literalinclude:: ../tests/thread_test.py + +In light of the long discussion above, a few remarks are in order: This +code uses the standard Thread library and explicitly calls +:func:`epics.ca.use_initial_context` prior to any CA calls in the target +function. Also note that the :func:`run_test` function is first called +from the main thread, so that the initial CA context does belong to the +main thread. Finally, the :func:`epics.ca.use_initial_context` call in +:func:`run_test` above could be replaced with +:func:`epics.ca.create_context`, and run OK. + +The output from this will look like:: + + First, create a PV in the main thread: + Run 2 Background Threads simultaneously: + -> thread "A" will run for 3.000 sec, monitoring ['Py:ao1', 'Py:ai1', 'Py:long1'] + -> thread "B" will run for 6.000 sec, monitoring ['Py:ai1', 'Py:long1', 'Py:ao2'] + Py:ao1 = 8.3948 (A) + Py:ai1 = 3.14 (B) + Py:ai1 = 3.14 (A) + Py:ao1 = 0.7404 (A) + Py:ai1 = 4.07 (B) + Py:ai1 = 4.07 (A) + Py:long1 = 3 (B) + Py:long1 = 3 (A) + Py:ao1 = 13.0861 (A) + Py:ai1 = 8.49 (B) + Py:ai1 = 8.49 (A) + Py:ao2 = 30 (B) + Completed Thread A + Py:ai1 = 9.42 (B) + Py:ao2 = 30 (B) + Py:long1 = 4 (B) + Py:ai1 = 3.35 (B) + Py:ao2 = 31 (B) + Py:ai1 = 4.27 (B) + Py:ao2 = 31 (B) + Py:long1 = 5 (B) + Py:ai1 = 8.20 (B) + Py:ao2 = 31 (B) + Completed Thread B + Done + +Note that while both threads *A* and *B* are running, a callback for the +PV `Py:ai1` is generated in each thread. + +Note also that the callbacks for the PVs created in each thread are +**explicitly cleared** with:: + + [p.clear_callbacks() for p in pvs] + +Without this, the callbacks for thread *A* will persist even after the +thread has completed! + + +.. index:: Multiprocessing +.. _advanced-multiprocessing-label: + +Using Multiprocessing with PyEpics +=========================================== + +An alternative to Python threads that has some very interesting and +important features is to use multiple *processes*, as with the standard +Python :mod:`multiprocessing` module. While using multiple processes has +some advantages over threads, it also has important implications for use +with PyEpics. The basic issue is that multiple processes need to be fully +separate, and do not share global state. For epics Channel Access, this +means that all those things like established communication channels, +callbacks, and Channel Access **context** cannot easily be share between +processes. + +The solution is to use a :class:`CAProcess`, which acts just like +:class:`multiprocessing.Process`, but knows how to separate contexts +between processes. This means that you will have to create PV objects for +each process (even if they point to the same PV). + +.. class:: CAProcess(group=None, target=None, name=None, args=(), kwargs={}) + + a subclass of :class:`multiprocessing.Process` that clears the global + Channel Access context before running you target function in its own + process. + +.. class:: CAPool(processes=None, initializer=None, initargs=(), maxtasksperchild=None) + + a subclass of :class:`multiprocessing.pool.Pool`, creating a Pool of + :class:`CAProcess` instances. + + +A simple example of using multiprocessing successfully is given: + + +.. literalinclude:: ../tests/test_multiprocessing.py + +here, the main process and the subprocess can each interact with the same +PV, though they need to create a separate connection (here, using :class:`PV`) +in each process. + +Note that different :class:`CAProcess` instances can communicate via +standard :class:`multiprocessing.Queue`. At this writing, no testing has +been done on using multiprocessing Managers. diff --git a/doc/alarm.rst b/doc/alarm.rst new file mode 100644 index 0000000..20f79da --- /dev/null +++ b/doc/alarm.rst @@ -0,0 +1,84 @@ +================================================ +Alarms: respond when a PV goes out of range +================================================ + +Overview +=========== + +.. module:: alarm + :synopsis: respond when a PV goes out of range by running user-supplied code + +The :mod:`alarm` module provides an Alarm object to specify an alarm +condition and what to do when that condition is met. + +.. class:: Alarm(pvname[, comparison=None[, trip_point=None[, callback=None[, alert_delay=10]]]]) + +creates an alarm object. + + :param pvname: name of Epics PV (string) + :param comparison: operation used to compare PV value to trip_point. + :type comparison: string or callable. Built in comparisons are listed in :ref:`Table of Alarm Operators`. + :param trip_point: value that will trigger the alarm + + :param callback: user-defined callback function to be run when the PVs value meets the alarm condition + :type callback: callable or None + :param alert_delay: time (in seconds) to wait before executing another alarm callback. + +The alarm works by checking the value of the PV each time it changes. If +the new value is outside the acceptable range (violates the trip point), +then the user-supplied callback function is run. This callback could be +set do send a message or to take some other course of action. + +The comparison supplied can either be a string as listed in :ref:`Table of +Alarm Operators` or a custom callable function which takes +the two values (PV.value, trip_point) and returns ``True`` or ``False`` +based on those values. + +.. _alarmops_table: + + Table of built-in Operators for Alarms: + + =============== ============================== + *operator* Python operator + =============== ============================== + 'eq', '==' __eq__ + 'ne', '!=' __ne__ + 'le', '<=' __le__ + 'lt', '<' __lt__ + 'ge', '>=' __ge__ + 'gt', '>' __gt__ + =============== ============================== + + +The :attr:`alert_delay` prevents the alarm callback from being called too +many times. For PVs with floating point values, the value may +fluctuate around the trip_point for a while. If the value violates the +trip_point, then momentarily goes back to an acceptable value, and back +again to a violating value, it may not be desirable to send repeated, +identical messages. To prevent this situation, the alarm callback will be +called when the alarm condition is met **and** the callback was not called +within the time specified by :attr:`alert_delay`. + + +Alarm Example +=============== + +An epics Alarm is very easy to use. Here is an alarm set to print a +message when a PV's value reaches a certain value:: + + from epics import Alarm, poll + + def alertMe(pvname=None, char_value=None, **kw): + print "Soup's on! %s = %s" % (pvname, char_value) + + my_alarm = Alarm(pvname = 'WaterTemperature.VAL', + comparison = '>', + callback = alertMe, + trip_point = 100.0, + alert_delay = 600) + while True: + poll() + + + + diff --git a/doc/arrays.rst b/doc/arrays.rst new file mode 100644 index 0000000..a8736e8 --- /dev/null +++ b/doc/arrays.rst @@ -0,0 +1,189 @@ +.. _arrays-label: + +============================================ +Working with waveform / array data +============================================ + +Though most EPICS Process Variables hold single values, PVs can hold array +data from EPICS waveform records. These are always data of a homogenous +data type, and have a fixed maximum element count (defined when the +waveform is created from the host EPICS process). Epics waveforms are +most naturally mapped to Arrays from the `numpy module +`_, and this is strongly encouraged. + +Arrays without Numpy +~~~~~~~~~~~~~~~~~~~~~~~~ + +If you have numpy installed, and use the default *as_numpy=True* in +: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 +`ctypes module `_. +These objects are not normally meant for casual use, but are not too +difficult to work with either. They can be easily converted to a simple +Python list with something like:: + + >>> import epics + >>> epics.ca.HAS_NUMPY = False # turn numpy off for session + >>> p = epics.PV('XX:scan1.P1PA') + >>> p.get() + + >>> ldat = list(p.get()) + +Note that this conversion to a list can be very slow for large arrays. + + +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:`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. +For example:: + + >>> import epics + >>> p = epics.PV('Py:double2k') + >>> print p + + >>> import numpy + >>> p.put(numpy.arange(10)/5.0) + >>> print p.get() + array([ 0. , 0.2, 0.4, 0.6, 0.8, 1. , 1.2, 1.4, 1.6, 1.8]) + +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 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 +~~~~~~~~~~~~~~~~~~~~~~~~ + +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:`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:: + + + record(waveform,"$(P):filename") { + field(DTYP,"Soft Channel") + field(DESC,"file name") + field(NELM,"128") + field(FTVL,"CHAR") + } + +Then you can use this record with: + + >>> import epics + >>> pvname = 'PREFIX:filename.VAL' + >>> pv = epics.PV(pvname) + >>> print pv.info + .... + >>> plain_val = pv.get() + >>> print plain_val + 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]) + >>> char_val = pv.get(as_string=True) + >>> print char_val + 'T:\\xas_user\\March2010\\FastMap' + +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 +of bytes. The conversion to a string assumes the ASCII character set. +Unicode is not directly supported. If you are storing non-ASCII data, you +would have to convert the raw array data yourself, perhaps like this (for +Python3):: + + >>> arr_data = pv.get() + >>> arr_bytes = bytes(list(array_data)) + >>> arr_string = str(arr_bytes, 'LATIN-1') + + +.. _arrays-large-label: + +Strategies for working with large arrays +============================================ + +EPICS Channels / Process Variables usually have values that can be stored +with a small number of bytes. This means that their storage and transfer +speeds over real networks is not a significant concern. However, some +Process Variables can store much larger amounts of data (say, several +megabytes) which means that some of the assumptions about dealing with +Channels / PVs may need reconsideration. + +When using PVs with large array sizes (here, I'll assert that *large* means +more than a few thousand elements), it is necessary to make sure that the +environmental variable ``EPICS_CA_MAX_ARRAY_BYTES`` is suitably set. +Unfortunately, this represents a pretty crude approach to memory management +within Epics for handling array data as it is used not only sets how large +an array the client can accept, but how much memory will be allocated on +the server. In addition, this value must be set prior to using the CA +library -- it cannot be altered during the running of a CA program. + +Normally, the default value for ``EPICS_CA_MAX_ARRAY_BYTES`` is 16384 (16k, +and it turns out that you cannot set it smaller than this value!). As +Python is used for clients, generally running on workstations or servers +with sufficient memory, this default value is changed to 2**24, or 16Mb) +when :mod:`epics.ca` is initialized. If the environmental variable +``EPICS_CA_MAX_ARRAY_BYTES`` has not already been set. + +The other main issue for PVs holding large arrays is whether they should be +automatically monitored. For PVs holding scalar data or small arrays, any +penalty for automatically monitoring these variables (that is, causing +network traffic every time a PV changes) is a small price to pay for being +assured that the latest value is always available. As arrays get larger +(as for data streams from Area Detectors), it is less obvious that +automatic monitoring is desirable. + +The Python :mod:`epics.ca` module defines a variable +: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:`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) + + +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 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 + >>> import Image + >>> pvname = '13IDCPS1:image1:ArrayData' + >>> img_pv = epics.PV(pvname) + >>> + >>> raw_image = img_pv.get() + >>> im_mode = 'RGB' + >>> im_size = (1360, 1024) + >>> img = Image.frombuffer(im_mode, im_size, raw_image, + 'raw', im_mode, 0, 1) + >>> img.show() + +The result looks like this (taken with a Prosilica GigE camera): + +.. image:: AreaDetector1.png + + +A more complete application for reading and displaying image from Epics +Area Detectors is included at `http://github.com/pyepics/epicsapps/ +`_. diff --git a/doc/autosave.rst b/doc/autosave.rst new file mode 100644 index 0000000..c668620 --- /dev/null +++ b/doc/autosave.rst @@ -0,0 +1,163 @@ + +========================================== +Auto-saving: simple save/restore of PVs +========================================== + +.. module:: autosave + :synopsis: simple save/restore of PVs + +The :mod:`autosave` module provides simple save/restore functionality for +PVs, with the functions :func:`save_pvs` and :func:`restore_pvs`, and an +:class:`AutoSaver` class. These are similar to the autosave module from +synApps for IOCs in that they use a compatible *request file* which +describes the PVs to save, and a compatible *save file* which holds the +saved values. Of course, the reading and writing is done here via Channel +Access, and need not be related to an single IOC. + +Use of this module requires the `pyparsing package +`_ to be installed. This is a fairly +common third-party python package, included in many package managers, or +installed with tools such as *easy_install* or *pip*, or downloaded from +`PyPI `_ + +Request and Save file formats are designed to be compatible with synApps +autosave. Notably, the `file` command with macro substitutions are +supported, so that one can have a Request like:: + + # My.req + file "SimpleMotor.req", P=IOC:, Q=m1 + +with a **SimpleMotor.req** file of:: + + # SimpleMotor.req + $(P)$(Q).VAL + $(P)$(Q).DIR + $(P)$(Q).FOFF + +which can then be used for many instances of a SimpleMotor. There is, +however, no automated mechanism for finding request files. You will need +to include these in the working directory or specify absolute paths. + +With such a file, simply using:: + + import epics.autosave + epics.autosave.save_pvs("My.req", "my_values.sav") + +will save the current values for the PVs to the file **my_values.sav**. At +a later time, these values can be restored with:: + + import epics.autosave + epics.autosave.restore_pvs("my_values.sav") + +The saved file will be of nearly identical format as that of the autosave +mechanism, and the :func:`restore_pvs` function can read and restore values +using save files from autosave. Note, however, that the purpose here is +quite different from that of the standard autosave module (which is +designed to save vales so that PVs can be **initialized** at IOC startup). +Using the functions here will really do a :func:`caput` to the saved +values. + + +.. function:: save_pvs(request_file, save_file) + + saves current value of PVs listed in *request_file* to the *save_file* + + :param request_file: name of Request file to read PVs to save. + :param save_file: name of file to save values to write values to + + As discussed above, the **request_file** follows the conventions of the + autosave module from synApps. + +.. function:: restore_pvs(save_file) + + reads values from *save_file* and restores them for the corresponding PVs + + :param save_file: name of file to save values to read data from. + + + Note that :func:`restore_pvs` will restore all the values it can, skipping + over any values that it cannot restore. + + +:class:`AutoSaver` class +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The :class:`AutoSaver` class provides a convenient way to repeatedly save +PVs listed in a request file without having to re-connect all of the PVs. +The :class:`AutoSaver` retains the PV connections, and provides a simple +:meth:`save` method to save the current PV values to a file. By default, +that file will be named from the request file and the current time. This +allows you to do something like this:: + + #!/usr/bin/env python + # save PVs from a request file once per minute + import time + from epics.autosave import AutoSaver + my_saver = AutoSaver("My.req") + + # save all PVs every minute for a day + t0 = time.time() + while True: + if time.localtime().tm_sec < 5: + my_saver.save() + time.sleep(30 - time.localtime().tm_sec) + if time.time() - t0 > 86400.0: + break + time.sleep(0.5) + +This will save PVs to files with names like *My_2017Oct02_141800.sav* + +.. class:: AutoSaver(request_file) + + create an Automatic Saver based on a request file. + + :param request_file: name of request file + +:class:`AutoSaver` has two methods: :meth:`read_request_file` to read a +request file, and :meth:`save` to save the results. + + +.. method:: read_request_file(request_file) + + read and parse request file, begin making PV connections + + :param request_file: name of request file + +.. method:: save(save_file=None, verbose=False) + + read current PV values, write save file. + + :param save_file: name of save file or `None`. If `None`, the name of + the request file and timestamp (to seconds) will be + used to build a file name. Note that there is no + check for overwriting files. + :param verbose: whether to print results to the screen [default `False`] + + + +Supported Data Types +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +All scalar PV values can be saved and restored with the :mod:`autosave` +routines. There is some support for waveform (array) data. For example, +character waveforms containing for long strings can be saved and restored. +In addition, numerical arrays in waveform can be saved and restored. For +array data, the results may not be fully compatible with the autosave +module. + + +Examples +========== + +A simple example using the autosave module:: + + import epics.autosave + # save values + epics.autosave.save_pvs("my_request_file.req", + "/tmp/my_recent_save.sav") + + # wait 30 seconds + time.sleep(30) + + # restore those values back + epics.autosave.restore_pvs("/tmp/my_recent_save.sav") diff --git a/doc/ca.rst b/doc/ca.rst new file mode 100644 index 0000000..7ca9884 --- /dev/null +++ b/doc/ca.rst @@ -0,0 +1,746 @@ +================================================= +ca: 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 +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 :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 +~~~~~~~~~~~~~ + +As a general rule, a CA function named `ca_XXX` in the C library will have the +equivalent function called `XXX` in the `ca` module. This is because the +intention is that one will import the `ca` module with + + >>> from epics import ca + +so that the Python function :func:`ca.XXX` will corresponds to the C +function `ca_XXX`. That is, the CA library called its functions `ca_XXX` +because C does not have namespaces. Python does have namespaces, and so +they are used. + +Similar name *un-mangling* also happens with the DBR prefixes for +constants, held here in the `dbr` module. Thus, the C constant DBR_STRING +becomes dbr.STRING in Python. + + +Other Changes and Omissions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Several function in the C version of the CA library are not implemented in +the Python module. Most of these unimplemented functions are currently +seen as unnecessary for Python, though some of these could be added without +much trouble if needed. See :ref:`ca-omissions-label` for further details. + +In addition, while the CA library supports several `DBR` types in C, not +all of these are supported in Python. Only native types and their DBR_TIME +and DBR_CTRL variants are supported here. The DBR_STS and DBR_GR variants +are not, as they are subsets of the DBR_CTRL type, and space optimization +is not something you'll be striving for with Python. Several `dbr_XXX` +functions are also not supported, as they appear to be needed only to be +able to dynamically allocate memory, which is not necessary in Python. + + +.. _ca-init-label: + +Initialization, Finalization, and Life-cycle +============================================== + +The Channel Access library must be initialized before it can be used. +There are 3 main reasons for this need: + + 1. CA requires a context model (preemptive callbacks or non-preemptive + callbacks) to be specified before any actual calls can be made. + + 2. the ctypes interface requires that the shared library be loaded + before it is used. + + 3. ctypes also requires that references to the library and callback + functions be kept for the life-cycle of CA-using part of a program (or + else they will be garbage collected). + +As far as is possible, the :mod:`ca` module hides the details of the CA +lifecyle from the user, so that it is not necessary to to worry about +explicitly initializing a Channel Access session. Instead, the library is +initialized as soon as it is needed, and intervention is really only +required to change default settings. The :mod:`ca` module also handles +finalizing the CA session, so that core-dumps and warning messages do not +happen due to CA still being 'alive' as a program ends. + +Because some users may wish to customize the initialization and +finalization process, the detailed steps will be described here. These +initialization and finalization tasks are handled in the following way: + + * The :data:`libca` variable in the :mod:`ca` module holds a permanent, + global reference to the CA shared object library (DLL). + + * the function :func:`initialize_libca` is called to initialize libca. + This function takes no arguments, but does use the global Boolean + :data:`PREEMPTIVE_CALLBACK` (default value of ``True``) to control + whether preemptive callbacks are used. + + * the function :func:`finalize_libca` is used to finalize libca. + Normally, this is function is registered to be called when a program + ends with :func:`atexit.register`. Note that this only gets called on + a graceful shutdown. If the program crashes (for a non-CA related + reason, for example), this finalization may not be done, and + connections to Epics Variables may not be closed completely on the + Channel Access server. + +.. data:: PREEMPTIVE_CALLBACK + + sets whether preemptive callbacks will be used. The default value is + ``True``. If you wish to run without preemptive callbacks this variable + *MUST* be set before any other use of the CA library. With preemptive + callbacks enabled, EPICS communication will not require client code to + continually poll for changes. With preemptive callback disables, you + will need to frequently poll epics with :func:`pend_io` and + func:`pend_event`. + +.. data:: DEFAULT_CONNECTION_TIMEOUT + + sets the default `timeout` value (in seconds) for + :func:`connect_channel`. The default value is `2.0` + +.. data:: AUTOMONITOR_MAXLENGTH + + sets the default array length (ie, how many elements an array has) above + which automatic conversion to numpy arrays *and* automatic monitoring + for PV variables is suppressed. The default value is 65536. To be + clear: waveforms with fewer elements than this value will be + automatically monitored changes, and will be converted to numpy arrays + (if numpy is installed). Larger waveforms will not be automatically + monitored. + + :ref:`arrays-label` and :ref:`arrays-large-label` for more details. + +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:: finalize_libca() + +.. autofunction:: pend_io(timeout=1.0) + +.. autofunction:: pend_event(timeout=1.e-5) + +.. autofunction:: poll(evt=1.e-5[, iot=1.0]) + +.. autofunction:: create_context() + +.. autofunction:: destroy_context() + +.. autofunction:: current_context() + +.. autofunction:: attach_context(context) + +.. autofunction:: detach_context() + +.. autofunction:: use_initial_context() + +.. autofunction:: client_status(context, level) + +.. autofunction:: version() + +.. autofunction:: message(status) + +.. autofunction:: flush_io() + +.. autofunction:: replace_printf_handler(fcn=None) + +.. warning:: + + `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. + + + +Creating and Connecting to Channels +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The basic channel object is the Channel ID or ``chid``. With the CA +library (and ``ca`` module), one creates and acts on the ``chid`` values. +These are simply :data:`ctypes.c_long` (C long integers) that hold the +memory address of the C representation of the channel, but it is probably +a good idea to treat these as object instances. + +.. autofunction:: create_channel(pvname, connect=False, callback=None, auto_cb=True) + +.. 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: + +.. autofunction:: name(chid) + +.. autofunction:: host_name(chid) + +.. autofunction:: element_count(chid) + +.. autofunction:: replace_access_rights_event(chid, callback=None) + +.. autofunction:: read_access(chid) + +.. autofunction:: write_access(chid) + +.. autofunction:: field_type(chid) + +See the *ftype* column from :ref:`Table of DBR Types `. + +.. autofunction:: clear_channel(chid) + +.. autofunction:: state(chid) + + +A few additional pythonic functions have been added: + +.. autofunction:: isConnected(chid) + +.. autofunction:: access(chid) + +.. autofunction:: promote_type(chid, [use_time=False, [use_ctrl=False]]) + +See :ref:`Table of DBR Types `. + +.. data:: _cache + + The ca module keeps a global cache of Channels that holds connection + status and a bit of internal information for all known PVs. This cache + is not intended for general use. + +.. autofunction:: show_cache(print_out=True) + + +.. autofunction:: clear_cache() + + + +Interacting with Connected Channels +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Once a ``chid`` is created and connected there are several ways to +communicating with it. These are primarily encapsulated in the functions +:func:`get`, :func:`put`, and :func:`create_subscription`, with a few +additional functions for retrieving specific information. + +These functions are where this python module differs the most from the +underlying CA library, and this is mostly due to the underlying CA function +requiring the user to supply DBR TYPE and count as well as ``chid`` and +allocated space for the data. In python none of these is needed, and +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:`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-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. + +.. 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) + +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) + +See :ref:`ca-callbacks-label` for more on writing the user-supplied callback, + +.. warning:: + + *event_id* is the id for the event (useful for clearing a subscription). + You **must** keep the returned tuple in active variables, either as a + global variable or as data in an encompassing class. + If you do *not* keep this data, the return value will be garbage + collected, the C-level reference to the callback will disappear, and you + will see coredumps. + + On Linux, a message like:: + + python: Objects/funcobject.c:451: func_dealloc: Assertion 'g->gc.gc_refs != (-2)' failed. + Abort (core dumped) + + is a hint that you have *not* kept this data. + + +.. data:: DEFAULT_SUBSCRIPTION_MASK + + This value is the default subscription type used when calling + :func:`create_subscription` with `mask=None`. It is also used by + default when creating a :class:`PV` object with auto_monitor is set + to ``True``. + + The initial default value is *dbr.DBE_ALARM|dbr.DBE_VALUE* + (i.e. update on alarm changes or value changes which exceeds the + monitor deadband.) The other possible flag in the bitmask is + *dbr.DBE_LOG* for archive-deadband changes. + + If this value is changed, it will change the default for all + subsequent calls to :func:`create_subscription`, but it will not + change any existing subscriptions. + +.. autofunction:: clear_subscription(event_id) + +Several other functions are provided: + +.. autofunction:: get_timestamp(chid) + +.. autofunction:: get_severity(chid) + +.. autofunction:: get_precision(chid) + +.. autofunction:: get_enum_strings(chid) + +.. autofunction:: get_ctrlvars(chid) + +See :ref:`Table of Control Attributes ` + +.. _ctrlvars_table: + + Table of Control Attributes + + ==================== ============================== + *attribute* *data types* + ==================== ============================== + status + severity + precision 0 for all but double, float + units + enum_strs enum only + upper_disp_limit + lower_disp_limit + upper_alarm_limit + lower_alarm_limit + upper_warning_limit + lower_warning_limit + upper_ctrl_limit + lower_ctrl_limit + ==================== ============================== + +Note that *enum_strs* will be a tuple of strings for the names of ENUM +states. + +.. autofunction:: get_timevars(chid) + + +.. _ca-sg-label: + +Synchronous Groups +~~~~~~~~~~~~~~~~~~~~~~~ + +.. 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 +finally block while all the channel access communication is done for the +group as a unit. It is important to *not* issue :func:`pend_io` during the +building of a synchronous group, as this will cause pending :func:`sg_put` +and :func:`sg_get` to execute. + +.. autofunction:: sg_create() + +.. autofunction:: sg_delete(gid) + +.. autofunction:: sg_block(gid[, timeout=10.0]) + +.. autofunction:: sg_get(gid, chid[, ftype=None[, as_string=False[, as_numpy=True]]]) + +.. autofunction:: sg_put(gid, chid, value) + +.. autofunction:: sg_test(gid) + +.. autofunction:: sg_reset(gid) + + +.. _ca-implementation-label: + +Implementation details +================================ + +The details given here should mostly be of interest to those looking at the +implementation of the `ca` module, those interested in the internals, or +those looking to translate lower-level C or Python code to this module. + +DBR data types +~~~~~~~~~~~~~~~~~ + +.. _dbrtype_table: + + Table of DBR Types + + ============== =================== ======================== + *CA type* *integer ftype* *Python ctypes type* + ============== =================== ======================== + string 0 string + int 1 integer + short 1 integer + float 2 double + enum 3 integer + char 4 byte + long 5 integer + double 6 double + + time_string 14 + time_int 15 + time_short 15 + time_float 16 + time_enum 17 + time_char 18 + time_long 19 + time_double 20 + ctrl_string 28 + ctrl_int 29 + ctrl_short 29 + ctrl_float 30 + ctrl_enum 31 + ctrl_char 32 + ctrl_long 33 + ctrl_double 34 + ============== =================== ======================== + +`PySEVCHK` and ChannelAccessExcepction: checking CA return codes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. exception:: ChannelAccessException + + This exception is raised when the :mod:`ca` module experiences + unexpected behavior and must raise an exception + +.. autofunction:: PySEVCHK(func_name, status[, expected=dbr.ECA_NORMAL]) + +.. autofunction:: withSEVCHK(fcn) + + +Function Decorators +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In addition to :func:`withSEVCHK`, several other decorator functions are +used heavily inside of ca.py or are available for your convenience. + +.. autofunction:: withCA(fcn) + +.. autofunction:: withCHID(fcn) + +.. autofunction:: withConnectedCHID(fcn) + +.. autofunction:: withInitialContext(fcn) + +See :ref:`advanced-threads-label` for further discussion. + + +Unpacking Data from Callbacks +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Throughout the implementation, there are several places where data returned +by the underlying CA library needs to be be converted to Python data. This +is encapsulated in the :func:`_unpack` function. In general, you will not +have to run this code, but there is one exception: when using +:func:`sg_get`, the values returned will have to be unpacked with this +function. + +.. autofunction:: _unpack(chid, data[, count=None[, ftype=None[, as_numpy=None]]]) + +.. _ca-callbacks-label: + +User-supplied Callback functions +================================ + +User-supplied callback functions can be provided for :func:`put`, +:func:`replace_access_rights_event`, and :func:`create_subscription`. +Note that callbacks for `PV` objects are slightly different: see +:ref:`pv-callbacks-label` in the :mod:`pv` module for details. + +When defining a callback function to be run either when a :func:`put` completes +or on changes to the Channel, as set from :func:`create_subscription`, or when +read/write permissions change from :func:`replace_access_rights_event`, it is +important to know two things: + + 1) how your function will be called. + 2) what is permissible to do inside your callback function. + +Callbacks will be called with keyword arguments for :func:`put` and for +:func:`create_subscription`. You should be prepared to have them passed to +your function. Use `**kw` unless you are very sure of what will be sent. +For the case of :func:`replace_access_rights_event`, only positional arguments +will be passed. + +For callbacks sent when a :func:`put` completes, your function will be passed these: + + * `pvname` : the name of the pv + * `data`: the user-supplied callback_data (defaulting to ``None``). + +For subscription callbacks, your function will be called with keyword/value +pairs that will include: + + * `pvname`: the name of the pv + * `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) + * `chid`: the integer address for the channel ID. + +For access rights event callbacks, your function will be passed: + + * `read_access`: boolean indicating read access status + * `write_access`: boolean indicating write access status + +Depending on the data type, and whether the CTRL or TIME variant was used, +the callback function may also include some of these as keyword arguments: + + * `enum_strs`: the list of enumeration strings + * `precision`: number of decimal places of precision. + * `units`: string for PV units + * `severity`: PV severity + * `timestamp`: timestamp from CA server. + * `posixseconds`: integer seconds since POSIX epoch of timestamp from CA server. + * `nanoseconds`: integer nanoseconds of timestamp from CA server. + +Note that a the user-supplied callback will be run *inside* a CA function, +and cannot reliably make any other CA calls. It is helpful to think "this +all happens inside of a :func:`pend_event` call", and in an epics thread +that may or may not be the main thread of your program. It is advisable to +keep the callback functions short and not resource-intensive. Consider +strategies which use the callback only to record that a change has occurred +and then act on that change later -- perhaps in a separate thread, perhaps +after :func:`pend_event` has completed. + +.. _ca-omissions-label: + +Omissions +========= + +Several parts of the CA library are not implemented in the Python module. +These are currently seen as unneeded (with notes where appropriate for +alternatives), though they could be added on request. + +.. function:: ca_add_exception_event + + *Not implemented*: Python exceptions are raised where appropriate and + can be used in user code. + +.. function:: ca_add_fd_registration + + *Not implemented* + +.. function:: ca_client_status + + *Not implemented* + +.. function:: ca_set_puser + + *Not implemented* : it is easy to pass user-defined data to callbacks as needed. + +.. function:: ca_puser + + *Not implemented*: it is easy to pass user-defined data to callbacks as needed. + +.. function:: ca_SEVCHK + + *Not implemented*: the Python function :func:`PySEVCHK` is + approximately the same. + +.. function:: ca_signal + + *Not implemented*: the Python function :func:`PySEVCHK` is + approximately the same. + +.. function:: ca_test_event + + *Not implemented*: this appears to be a function for debugging events. + These are easy enough to simulate by directly calling Python callback + functions. + +.. function:: ca_dump_dbr + + *Not implemented* + +In addition, not all `DBR` types in the CA C library are supported. + +Only native types and their DBR_TIME and DBR_CTRL variants are supported: +DBR_STS and DBR_GR variants are not. Several `dbr_XXX` functions are also +not supported, as they are needed only to dynamically allocate memory. + +:class:`CAThread` class +========================== + +.. class:: CAThread(group=None[, target=None[, name=None[, args=()[, kwargs={}]]]]) + + create a CA-aware subclass of a standard Python :class:`threading.Thread`. See the + standard library documentation for further information on how to use Thread objects. + + A `CAThread` simply runs :func:`use_initial_context` prior to running each target + function, so that :func:`use_initial_context` does not have to be explicitly put inside + the target function. + + The See :ref:`advanced-threads-label` for further discussion. + + +Examples +========= + +Here are some example sessions using the :mod:`ca` module. + +Create, Connect, Get Value of Channel +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Note here that several things have been simplified compare to using CA in C: +initialization and creating a main-thread context are handled, and connection +of channels is handled in the background:: + + from epics import ca + chid = ca.create_channel('XXX:m1.VAL') + count = ca.element_count(chid) + ftype = ca.field_type(chid) + print "Channel ", chid, count, ftype + value = ca.get() + print value + +Put, waiting for completion +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Here we set a PVs value, waiting for it to complete:: + + from epics import ca + chid = ca.create_channel('XXX:m1.VAL') + ca.put(chid, 1.0, wait=True) + +The :func:`put` method will wait to return until the processing is +complete. + +Define a callback to Subscribe to Changes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Here, we *subscribe to changes* for a PV, which is to say we define a +callback function to be called whenever the PV value changes. In the case +below, the function to be called will simply write the latest value out to +standard output:: + + from epics import ca + import time + import sys + + # define a callback function. Note that this should + # expect certain keyword arguments, including 'pvname' and 'value' + def onChanges(pvname=None, value=None, **kw): + fmt = 'New Value: %s value=%s, kw=%s\n' + sys.stdout.write(fmt % (pvname, str(value), repr(kw))) + sys.stdout.flush() + + # create the channel + mypv = 'XXX.VAL' + chid = ca.create_channel(mypv) + + # subscribe to events giving a callback function + eventID = ca.create_subscription(chid, callback=onChanges) + + # now we simply wait for changes + t0 = time.time() + while time.time()-t0 < 10.0: + time.sleep(0.001) + +It is **vital** that the return value from :func:`create_subscription` is +kept in a variable so that it cannot be garbage collected. Failure to keep +this value will cause trouble, including almost immediate segmentation +faults (on Windows) or seemingly inexplicable crashes later (on linux). + +Define a connection callback +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Here, we define a connection callback -- a function to be called when the +connection status of the PV changes. Note that this will be called on +initial connection:: + + import epics + import time + + def onConnectionChange(pvname=None, conn=None, chid=None): + print 'ca connection status changed: ', pvname, conn, chid + + # create channel, provide connection callback + motor1 = '13IDC:m1' + chid = epics.ca.create_channel(motor1, callback=onConnectionChange) + + print 'Now waiting, watching values and connection changes:' + t0 = time.time() + while time.time()-t0 < 30: + time.sleep(0.001) + +This will run the supplied callback soon after the channel has been +created, when a successful connection has been made. Note that the +callback should be prepared to accept keyword arguments of `pvname`, +`chid`, and `conn` for the PV name, channel ID, and connection state +(``True`` or ``False``). + +Define an access rights change event callback +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The following example demonstrates the addition of a function to be +called when the access rights of a channel changes. Note this will be +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) + + # create a channel and attach the above function for access rights events + chid = epics.ca.create_channel('pv_name') + # a message should be printed immediately following this registration + epics.ca.replace_access_rights_event(chid, callback=on_access_rights_change) + + # Affecting the channel's access rights, (for example, by enabling/disabling + # CA Security rules), should produce additional console messages + try: + while True: + time.sleep(0.25) + except KeyboardInterrupt: + pass diff --git a/doc/conf.py b/doc/conf.py new file mode 100644 index 0000000..e50affe --- /dev/null +++ b/doc/conf.py @@ -0,0 +1,205 @@ +# -*- coding: utf-8 -*- +# +# epics documentation build configuration file, created by +# sphinx-quickstart on Fri Feb 12 01:10:08 2010. +# +# This file is execfile()d with the current directory set to its containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys, os + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +#sys.path.append(os.path.abspath('.')) + +# -- General configuration ----------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.extlinks', + 'sphinx.ext.coverage', 'sphinx.ext.mathjax', 'numpydoc'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'epics' +copyright = u'2014, Matthew Newville' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +try: + import epics + release = epics.__version__ + if '-' in release: + a, b = release.split('-', 1) + release = a + if '_' in release: + a, b = release.split('_', 1) + release = a + # The full version, including alpha/beta/rc tags. +except ImportError: + release = '3.X.Y' + +print 'Building Docs for EPICS version %s / Python version %s ' % (release, sys.version) + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of documents that shouldn't be included in the build. +#unused_docs = [] + +# List of directories, relative to source directory, that shouldn't be searched +# for source files. +exclude_trees = ['_build'] + +# The reST default role (used for this markup: `text`) to use for all documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +# show_authors = True + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + + +# -- Options for HTML output --------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. Major themes that come with +# Sphinx are currently 'default' and 'sphinxdoc'. + +##### +html_theme_path = ['sphinx/theme'] +html_theme = 'epicsdoc' +##### + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None +html_title = 'Epics Channel Access for Python' + +# A shorter title for the navigation bar. Default is the same as html_title. +html_short_title = 'PyEpics' + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +# html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +html_sidebars = {'index': ['indexsidebar.html','searchbox.html']} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +# html_use_modindex = True +html_use_modindex = False + +# If false, no index is generated. +html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +html_show_sourcelink = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = '' + +# Output file base name for HTML help builder. +htmlhelp_basename = 'epicsdoc' + + +# -- Options for LaTeX output -------------------------------------------------- + +# The paper size ('letter' or 'a4'). +#latex_paper_size = 'letter' + +# The font size ('10pt', '11pt' or '12pt'). +#latex_font_size = '10pt' + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass [howto/manual]). +latex_documents = [ + ('index', 'epics.tex', u'PyEpics: Python Epics Channel Access', + u'Matthew Newville', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +latex_logo = '_static/pyepics.png' + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# Additional stuff for the LaTeX preamble. +#latex_preamble = '' + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +# latex_use_modindex = True +latex_use_modindex = False diff --git a/doc/devices.rst b/doc/devices.rst new file mode 100644 index 0000000..185b303 --- /dev/null +++ b/doc/devices.rst @@ -0,0 +1,581 @@ +================================ +Devices: collections of PVs +================================ + +Overview +=========== + +.. module:: device + :synopsis: collections of related PVs + +The :mod:`device` module provides a simple interface to a collection of +PVs. Here an epics :class:`device.Device` is an object holding a set of +PVs, all sharing a prefix, but having many *attributes*. Many PVs will +have names made up of *prefix+attribute*, with a common prefix for several +related PVs. This almost describes an Epics Record, but as it is concerned +only with PV names, the mapping to an Epics Record is not exact. On the +other hand, the concept of a *device* is more flexible than a predefined +Epics Record as it can actually hold PVs from several different records.:: + + motor1 = epics.Device('XXX:motor1.', attrs=('VAL', 'RBV', 'DESC', 'RVAL', + 'LVIO', 'HLS', 'LLS')) + motor1.put('VAL', 1) + print 'Motor %s = %f' % ( motor1.get('DESC'), motor1.get('RBV')) + + motor1.VAL = 0 + print 'Motor %s = %f' % ( motor1.DESC, motor1.RBV ) + +While useful on its own like this, the real point of a *device* is as a +base class, to be inherited and extended. In fact, there is a more +sophisticated Motor device described below at :ref:`device-motor-label` + +.. class:: Device(prefix=None[, delim=''[, attrs=None]]) + +The attribute PVs are built as needed and held in an internal buffer +:data:`self._pvs`. This class is kept intentionally simple so that it may +be subclassed. + +To pre-load attribute names on initialization, provide a list or tuple of +attributes with the `attr` option. + +Note that *prefix* is actually optional. When left off, this class can be +used as an arbitrary container of PVs, or to turn any subclass into an +epics Device. + +In general, PV names will be mapped as prefix+delim+attr. See +:meth:`add_pv` for details of how to override this. + +.. method:: PV(attr[, connect=True[, **kw]]]) + + returns the `PV` object for a device attribute. The connect argument + and any other keyword arguments are passed to :meth:`epics.PV`. + +.. method:: put(attr, value[, wait=False[, timeout=10.0]]) + + put an attribute value, optionally wait for completion or up to a + supplied timeout value + +.. method:: get(attr[, as_string=False]) + + get an attribute value, option as_string returns a string + representation + +.. method:: add_callback(attr, callback) + + add a callback function to an attribute PV, so that the callback + function will be run when the at tribute's value changes + +.. method:: add_pv(pvname[, attr=None,[ **kw]]) + + adds an explicitly names :meth:`epics.PV` to the device even though it + may violate the normal naming rules (in which `attr` is mapped to + `epics.PV(prefix+delim+attr)`. That is, one can say:: + + import epics + m1 = epics.Device('XXX:m1', delim='.') + m1.add_pv('XXX:m2.VAL', attr='other') + print m1.VAL # print value of XXX:m1.VAL + print m1.other # prints value of XXX:m2.VAL + + +.. method:: save_state() + + return a dictionary of all current values -- the ''current state''. + + +.. method:: restore_state(state) + + restores a saved state, as saved with :meth:`save_state` + +.. method:: write_state(fname[, state=None]) + + write a saved state to a file. If no state is provide, the current state is written. + +.. method:: read_state(fname[, restore=False]) + + reads a state from a file, as written with :meth:`write_state`, and returns it. + If ''restore'' is ``True``, the read state will be restored. + +.. data:: _pvs + + a dictionary of PVs making up the device. + + +.. _device-motor-label: + +Epics Motor Device +=========================== + +.. module:: motor + +The Epics Motor record has over 100 fields associated with it. Of course, +it is often preferable to think of 1 Motor with many attributes than 100 +or so separate PVs. Many of the fields of the Motor record are +interrelated and influence other settings, including limits on the range of +motion which need to be respected, and which may send notifications when +they are violated. Thus, there is a fair amount of functionality for a +Motor. Typically, the user just wants to move the motor by setting its +drive position, but a fully enabled Motor should allow the use to change +and read many of the Motor parameters. + +The :class:`Motor` class helps the user create and use Epics motors. +A simple example use would be:: + + import epics + m1 = epics.Motor('XXX:m1') + + print 'Motor: ', m1.DESC , ' Currently at ', m1.RBV + + m1.tweak_val = 0.10 + m1.move(0.0, dial=True, wait=True) + + for i in range(10): + m1.tweak(direction='forward', wait=True) + time.sleep(1.0) + print 'Motor: ', m1.DESC , ' Currently at ', m1.RBV + +Which will step the motor through a set of positions. You'll notice a +few features for Motor: + + 1. Motors can use English-name aliases for attributes for fields of the + motor record. Thus 'VAL' can be spelled 'drive' and 'DESC' can be + 'description'. The :ref:`Table of Motor Attributes ` + give the list of names that can be used. + + 2. The methods for setting positions can use the User, Dial, or Step + coordinate system, and can wait for completion. + + +The :class:`epics.Motor` class +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. class:: Motor(pvname[, timeout=30.]) + + create a Motor object for a named Epics Process Variable. + + :param pvname: prefix name (no '.VAL' needed!) of Epics Process Variable for a Motor + :type pvname: string + :param timeout: time (in seconds) to wait before giving up trying to connect. + :type timeout: float + +Once created, a Motor should be ready to use. + + >>> from epics import Motor + >>> m = Motor('XX:m1') + >>> print m.drive, m.description, m.slew_speed + 1.030 Fine X 5.0 + >>> print m.get('device_type', as_string=True) + 'asynMotor' + + +A Motor has very many fields. Only a few of them are created on +initialization -- the rest are retrieved as needed. The motor fields can +be retrieved either with an attribute or with the :meth:`get` method. +A full list of Motor attributes and their aliases for the motor +record is given in :ref:`Table of Motor Attributes `. + +.. _motorattr_table: + + Table of Aliases for attributes for the epics :class:`Motor` class, and the + corresponding attribute name of the Motor Record field. + + ++--------------------+--------------------------+---+--------------------+--------------------------+ +| **alias** | *Motor Record field* | | **alias** | *Motor Record field* | ++====================+==========================+===+====================+==========================+ +| disabled | _able.VAL | | moving | MOVN | ++--------------------+--------------------------+---+--------------------+--------------------------+ +| acceleration | ACCL | | resolution | MRES | ++--------------------+--------------------------+---+--------------------+--------------------------+ +| back_accel | BACC | | motor_status | MSTA | ++--------------------+--------------------------+---+--------------------+--------------------------+ +| backlash | BDST | | offset | OFF | ++--------------------+--------------------------+---+--------------------+--------------------------+ +| back_speed | BVEL | | output_mode | OMSL | ++--------------------+--------------------------+---+--------------------+--------------------------+ +| card | CARD | | output | OUT | ++--------------------+--------------------------+---+--------------------+--------------------------+ +| dial_high_limit | DHLM | | prop_gain | PCOF | ++--------------------+--------------------------+---+--------------------+--------------------------+ +| direction | DIR | | precision | PREC | ++--------------------+--------------------------+---+--------------------+--------------------------+ +| dial_low_limit | DLLM | | readback | RBV | ++--------------------+--------------------------+---+--------------------+--------------------------+ +| settle_time | DLY | | retry_max | RTRY | ++--------------------+--------------------------+---+--------------------+--------------------------+ +| done_moving | DMOV | | retry_count | RCNT | ++--------------------+--------------------------+---+--------------------+--------------------------+ +| dial_readback | DRBV | | retry_deadband | RDBD | ++--------------------+--------------------------+---+--------------------+--------------------------+ +| description | DESC | | dial_difference | RDIF | ++--------------------+--------------------------+---+--------------------+--------------------------+ +| dial_drive | DVAL | | raw_encoder_pos | REP | ++--------------------+--------------------------+---+--------------------+--------------------------+ +| units | EGU | | raw_high_limit | RHLS | ++--------------------+--------------------------+---+--------------------+--------------------------+ +| encoder_step | ERES | | raw_low_limit | RLLS | ++--------------------+--------------------------+---+--------------------+--------------------------+ +| freeze_offset | FOFF | | relative_value | RLV | ++--------------------+--------------------------+---+--------------------+--------------------------+ +| move_fraction | FRAC | | raw_motor_pos | RMP | ++--------------------+--------------------------+---+--------------------+--------------------------+ +| hi_severity | HHSV | | raw_readback | RRBV | ++--------------------+--------------------------+---+--------------------+--------------------------+ +| hi_alarm | HIGH | | readback_res | RRES | ++--------------------+--------------------------+---+--------------------+--------------------------+ +| hihi_alarm | HIHI | | raw_drive | RVAL | ++--------------------+--------------------------+---+--------------------+--------------------------+ +| high_limit | HLM | | dial_speed | RVEL | ++--------------------+--------------------------+---+--------------------+--------------------------+ +| high_limit_set | HLS | | s_speed | S | ++--------------------+--------------------------+---+--------------------+--------------------------+ +| hw_limit | HLSV | | s_back_speed | SBAK | ++--------------------+--------------------------+---+--------------------+--------------------------+ +| home_forward | HOMF | | s_base_speed | SBAS | ++--------------------+--------------------------+---+--------------------+--------------------------+ +| home_reverse | HOMR | | s_max_speed | SMAX | ++--------------------+--------------------------+---+--------------------+--------------------------+ +| high_op_range | HOPR | | set | SET | ++--------------------+--------------------------+---+--------------------+--------------------------+ +| high_severity | HSV | | stop_go | SPMG | ++--------------------+--------------------------+---+--------------------+--------------------------+ +| integral_gain | ICOF | | s_revolutions | SREV | ++--------------------+--------------------------+---+--------------------+--------------------------+ +| jog_accel | JAR | | stop | STOP | ++--------------------+--------------------------+---+--------------------+--------------------------+ +| jog_forward | JOGF | | t_direction | TDIR | ++--------------------+--------------------------+---+--------------------+--------------------------+ +| jog_reverse | JOGR | | tweak_forward | TWF | ++--------------------+--------------------------+---+--------------------+--------------------------+ +| jog_speed | JVEL | | tweak_reverse | TWR | ++--------------------+--------------------------+---+--------------------+--------------------------+ +| last_dial_val | LDVL | | tweak_val | TWV | ++--------------------+--------------------------+---+--------------------+--------------------------+ +| low_limit | LLM | | use_encoder | UEIP | ++--------------------+--------------------------+---+--------------------+--------------------------+ +| low_limit_set | LLS | | u_revolutions | UREV | ++--------------------+--------------------------+---+--------------------+--------------------------+ +| lo_severity | LLSV | | use_rdbl | URIP | ++--------------------+--------------------------+---+--------------------+--------------------------+ +| lolo_alarm | LOLO | | drive | VAL | ++--------------------+--------------------------+---+--------------------+--------------------------+ +| low_op_range | LOPR | | base_speed | VBAS | ++--------------------+--------------------------+---+--------------------+--------------------------+ +| low_alarm | LOW | | slew_speed | VELO | ++--------------------+--------------------------+---+--------------------+--------------------------+ +| last_rel_val | LRLV | | version | VERS | ++--------------------+--------------------------+---+--------------------+--------------------------+ +| last_dial_drive | LRVL | | max_speed | VMAX | ++--------------------+--------------------------+---+--------------------+--------------------------+ +| last_SPMG | LSPG | | use_home | ATHM | ++--------------------+--------------------------+---+--------------------+--------------------------+ +| low_severity | LSV | | deriv_gain | DCOF | ++--------------------+--------------------------+---+--------------------+--------------------------+ + + +methods for :class:`epics.Motor` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. method:: get(attr[, as_string=False]) + + sets a field attribute for the motor. + + :param attr: attribute name + :type attr: string (from table above) + :param as_string: whether to return string value. + :type as_string: ``True``/ ``False`` + +Note that :meth:`get` can return the string value, while fetching the +attribute cannot do so:: + + >>> m = epics.Motor('XXX:m1') + >>> print m.device_type + 0 + >>> print m.get('device_type', as_string=True) + 'asynMotor' + +.. method:: put(attr, value[, wait=False[, timeout=30]]) + + sets a field attribute for the motor. + + :param attr: attribute name + :type attr: string (from table above) + :param value: value for attribute + :param wait: whether to wait for completion. + :type wait: ``True``/``False`` + :param timeout: time (in seconds) to wait before giving up trying to connect. + :type timeout: float + + +.. method:: check_limits() + + checks whether the current motor position is causing a motor limit + violation, and raises a MotorLimitException if it is. + + returns ``None`` if there is no limit violation. + +.. method:: within_limits(value[, limits='user']) + + checks whether a target value **would be** a limit violation. + + :param value: target value + :param limits: one of 'user', 'dial', or 'raw' for which limits to consider + :type limits: string + :rtype: ``True``/``False`` + + +.. method:: move(val=None[, relative=None[, wait=False[, timeout=300.0[, dial=False[, raw=False[, ignore_limits=False, [confirm_move=False]]]]]]]) + + moves motor to specified drive position. + + :param val: value to move to (float) [Must be provided] + :param relative: move relative to current position (T/F) [F] + :param wait: whether to wait for move to complete (T/F) [F] + :param timeout: max time for move to complete (in seconds) [300] + :param dial: use dial coordinates (T/F) [F] + :param raw: use raw coordinates (T/F) [F] + :param ignore_limits: try move without regard to limits (T/F) [F] + :param confirm_move: try to confirm that move has begun (when wait=False) (T/F) [F] + :rtype: integer + + Returns an integer value, according the table below. Note that a return + value of 0 with `wait=False` does not really guarantee a successful + move, just that a move request was issued. If you're interested in + checking that a requested move really did start without waiting for the + move to complete, you may want to use the `confirm_move=True` option. + + +.. _motor_move_return_vals_table: + + Table of return values from :func:`move`. + + +---------------+----------------------------------------------------------------+ + | return value | meaning | + +===============+================================================================+ + | -13 | invalid value (cannot convert to float). Move not attempted. | + +---------------+----------------------------------------------------------------+ + | -12 | target value outside soft limits. Move not attempted. | + +---------------+----------------------------------------------------------------+ + | -11 | drive PV is not connected: Move not attempted. | + +---------------+----------------------------------------------------------------+ + | -8 | move started, but timed-out. | + +---------------+----------------------------------------------------------------+ + | -7 | move started, timed-out, but appears done. | + +---------------+----------------------------------------------------------------+ + | -5 | move started, unexpected return value from :func:`put` | + +---------------+----------------------------------------------------------------+ + | -4 | move-with-wait finished, soft limit violation seen. | + +---------------+----------------------------------------------------------------+ + | -3 | move-with-wait finished, hard limit violation seen. | + +---------------+----------------------------------------------------------------+ + | 0 | move-with-wait finish OK. | + +---------------+----------------------------------------------------------------+ + | 0 | move-without-wait executed, not 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. | + +---------------+----------------------------------------------------------------+ + + +.. method:: tweak(direction='forward'[, wait=False[, timeout=300.]]) + + move the motor by the current *tweak value* + + :param direction: direction of motion + :type direction: string: 'forward' (default) or 'reverse' + :param wait: whether to wait for completion + :type wait: ``True``/``False`` + :param timeout: max time for move to complete (in seconds) [default=300] + :type timeout: float + + +.. method:: get_position(readback=False[, dial=False[, raw=False]]) + + Returns the motor position in user, dial or raw coordinates. + + :param readback: whether to return the readback position in the + desired coordinate system. The default is to return the + drive position of the motor. + :param dial: whether to return the position in dial coordinates. + The default is user coordinates. + :param raw: whether to return the raw position. + The default is user coordinates. + + The "raw" and "dial" keywords are mutually exclusive. + The "readback" keyword can be used in user, dial or raw coordinates. + +.. method:: set_position(position[ dial=False[, raw=False]]) + + set (that is, redefine) the current position to supplied value. + + :param position: The new motor position + :param dial: whether to set in dial coordinates. The default is user coordinates. + :param raw: whether to set in raw coordinates. The default is user coordinates. + + The 'raw' and 'dial' keywords are mutually exclusive. + +.. method:: get_pv(attr) + + returns the `PV` for the corresponding attribute. + +.. method:: set_callback(attr='drive'[, callback=None[, kw=None]]) + + sets a callback on the `PV` for a particular attribute. + +.. method:: clear_callback(attr='drive') + + clears a callback on the `PV` for a particular attribute. + +.. method:: show_info() + + prints out a table of attributes and their current values. + + + +Other Device Examples +=========================== + +An epics Device provides a general way to group together a set of PVs. The +examples below show how to build on this generality, and may inspire you to +build your own device classes. + +A basic Device without a prefix +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Here, we define a very simple device that does not even define a prefix. +This is not much more than a collection of PVs. Since there is no prefix +given, all PVs in the device must be *fully qualified*. Note that there is +no requirement to share a common prefix in such a collection of PVs:: + + from epics import Device + dev = Device() + p1 = dev.PV('13IDC:m1.VAL') + p2 = dev.PV('13IDC:m2.VAL') + dev.put('13IDC:m1.VAL', 2.8) + dev.put('13IDC:m2.VAL', 3.0) + print dev.PV('13IDC:m3.DIR').get(as_string=True) + +Note that this device cannot use the attributes based on field names. + +This may not look very interesting -- why not just use a bunch of PVs? If +ou consider `Device` to be a starting point for building more complicated +objects by subclassing `Device` and adding specialized methods, then it can +start to get interesting. + + +Epics ai record as Device +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +For a slightly more useful and typical example, the pyepics distribution +includes a Device for an Epics ai (analog input record). The full +implementation of this device is: + + +.. literalinclude:: ../epics/devices/ai.py + +The code simply pre-defines the fields that are the *suffixes* of an Epics ai +input record, and subclasses :class:`Device` with these fields to create the +corresponding PVs. For most record suffixes, these will be available as +attributes of the Device object. For example, the :class:`ai` class above can +be used simply and cleanly as:: + + from epics.devices import ai + This_ai = ai('XXX.PRES') + print 'Value: ', This_ai.VAL + print 'Units: ', This_ai.EGU + +Of course, you can also use the :meth:`get`, :meth:`put` methods above for a +basic :class:`Device`:: + + This_ai.put('DESC', 'My Pump') + + +Several of the other standard Epics records can easily be exposed as Devices in +this way, and the pyepics distribution includes such simple wrappings for the +Epics ao, bi, and bo records, as well as several more complex records from +synApps. + +Epics Scaler Record as Device +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +For a slightly more complicated example: an incomplete, but very useful mapping +of the Scaler Record from synApps, including methods for changing modes, and +reading and writing data. + +.. literalinclude:: ../epics/devices/scaler.py + +Note that we can then create a :class:`scaler` object from its base PV +prefix, and use methods like :meth:`Count` and :meth:`Read` without +directly invoking epics calls:: + + s1 = Scaler('XXX:scaler1') + s1.setCalc(2, '(B-2000*A/10000000.)') + s1.enableCalcs() + s1.OneShotMode() + s1.Count(t=5.0, wait=True) + print 'Names: ', s1.getNames() + print 'Raw values: ', s1.Read(use_calc=False) + print 'Calc values: ', s1.Read(use_calc=True) + + +Other Devices included in PyEpics +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +Several other Epics Records have been exposed as Devices, and included in +PyEpics distribution. These vary some in how complete and feature-rich they +are, and are definitely skewed toward data collection at synchrotron beamlines. +A table of current Devices are listed in the :ref:`Table of Included Epics +Devices ` table below. For further details, consult the source +code for these modules. + +.. _devices_table: + + Table of Epics Devices Included in the PyEpics distribution. For those + described as "pretty basic", there are generally only PV suffixes to + attributes mapped. Many of the others include one or more methods for + specific use of that Device. + + ++----------------+-----------------+------------------------------------------------+ +| **module** | **class** | description | ++================+=================+================================================+ +| ad_base | AD_Camera | areaDetector Camera, pretty basic | ++----------------+-----------------+------------------------------------------------+ +| ad_fileplugin | AD_FilePlugin | areaDetector File Plugin, many methods | ++----------------+-----------------+------------------------------------------------+ +| ad_image | AD_ImagePlugin | areaDetector Image, with ArrayData attribute | ++----------------+-----------------+------------------------------------------------+ +| ad_overlay | AD_OverlayPlugin| areaDetector Overlay, pretty basic | ++----------------+-----------------+------------------------------------------------+ +| ad_perkinelmer | AD_PerkinElmer | PerkinElmer(xrd1600) detector, several methods | ++----------------+-----------------+------------------------------------------------+ +| ai | ai | analog input, pretty basic (as above) | ++----------------+-----------------+------------------------------------------------+ +| ao | ao | analog output, pretty basic | ++----------------+-----------------+------------------------------------------------+ +| bi | bi | binary input, pretty basic | ++----------------+-----------------+------------------------------------------------+ +| bo | bo | binary output, pretty basic | ++----------------+-----------------+------------------------------------------------+ +| mca | MCA | epics DXP record, pretty basic | ++----------------+-----------------+------------------------------------------------+ +| mca | DXP | epics MCA record, get_rois()/get_calib() | ++----------------+-----------------+------------------------------------------------+ +| mca | MultiXMAP | Multiple XIA XMaps, several methods | ++----------------+-----------------+------------------------------------------------+ +| scaler | Scaler | epics Scaler record, many methods | ++----------------+-----------------+------------------------------------------------+ +| scan | Scan | epics SScan record, some methods | ++----------------+-----------------+------------------------------------------------+ +| srs570 | SRS570 | SRS570 Amplifier | ++----------------+-----------------+------------------------------------------------+ +| struck | Struck | SIS Multichannel Scaler, many methods | ++----------------+-----------------+------------------------------------------------+ +| transform | Transform | epics userTransform record | ++----------------+-----------------+------------------------------------------------+ +| xspress3 | Xspress3 | Quantum Electronics Xspress3 Multi-MCA | ++----------------+-----------------+------------------------------------------------+ diff --git a/doc/index.rst b/doc/index.rst new file mode 100644 index 0000000..20472dc --- /dev/null +++ b/doc/index.rst @@ -0,0 +1,48 @@ +.. epics documentation master file + +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 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 +`http://pyepics.github.com/epicsapps/ +`_ for further details. + +----------- + +.. toctree:: + :maxdepth: 2 + + installation + overview + pv + ca + arrays + devices + alarm + autosave + wx + advanced diff --git a/doc/installation.rst b/doc/installation.rst new file mode 100644 index 0000000..9a8c342 --- /dev/null +++ b/doc/installation.rst @@ -0,0 +1,181 @@ + +==================================== +Downloading and Installation +==================================== + +Prerequisites +~~~~~~~~~~~~~~~ + +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 +to automatically convert between EPICS waveforms and numpy arrays. + +The `autosave` module requires the `pyparsing` package, which is widely +available and often installed by default with many Python distributions. +The `wx` module requires the `wxPython` package, and the `qt` module +requires `PyQt` or `PySide`. + + +Downloads and Installation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. _pyepics github repository: http://github.com/pyepics/pyepics +.. _Python Setup Tools: http://pypi.python.org/pypi/setuptools +.. _pyepics PyPi: https://pypi.python.org/pypi/pyepics/ +.. _pyepics CARS downloads: http://cars9.uchicago.edu/software/python/pyepics3/src/ + + +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 Python, there are a few conda channels for pyepics, +including:: + + conda install -c GSECARS pyepics + +You can also download the source package, unpack it, and install with:: + + python setup.py install + + +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. 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 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 + +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 + >>> epics.ca.find_libca() + +which will print out the full path of the CA dynamic library that will be used. + +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 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 +~~~~~~~~~~~~~ + +Automated and continuous unit-testing is done with the TravisCI +(https://travis-ci.org/pyepics/pyepics) for Python 2.7, 3.5, and 3.6 using +an Epics IOC running in a Docker image. Many tests located in the `tests` +folder can also be run using the script ``tests/simulator.py`` as long as +the Epics database in ``tests/pydebug.db`` is loaded in a local IOC. In +addition, tests are regularly run on Mac OSX, and 32-bit and 64-bit +Windows. + + +Development Version +~~~~~~~~~~~~~~~~~~~~~~~~ + +Development of pyepics is done through the `pyepics github +repository`_. To get a copy of the latest version do:: + + git clone git@github.com/pyepics/pyepics.git + + +Getting Help +~~~~~~~~~~~~~~~~~~~~~~~~~ + +For questions, bug reports, feature request, please consider using the +following methods: + + 1. Send email to the Epics Tech Talk mailing list. You can send mail + directly to Matt Newville , but the mailing + list has many Epics experts reading it, so someone else interested or + knowledgeable about the topic might provide an answer. Since the + mailing list is archived and the main mailing list for Epics work, a + question to the mailing list has a better chance of helping someone + else. + + 2. Create an Issue on http://github.com/pyepics/pyepics. Though the + github Issues seem to be intended for bug tracking, they are a fine + way to catalog various kinds of questions and feature requests. + + 3. If you are sure you have found a bug in existing code, or have + some code you think would be useful to add to pyepics, consider + making a Pull Request on http://github.com/pyepics/pyepics. + + +License +~~~~~~~~~~~~~~~~~~~ + +The pyepics source code, this documentation, and all material +associated with it are distributed under the Epics Open License: + +.. include:: ../LICENSE + +In plain English, this says that there is no warranty or guarantee that the +code will actually work, but you can do anything you like with this code +except a) claim that you wrote it or b) claim that the people who did write +it endorse your use of the code. Unless you're the US government, in which +case you can probably do whatever you want. + +Acknowledgments +~~~~~~~~~~~~~~~~~~~~~~ + +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, 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 new file mode 100644 index 0000000..a03bd06 --- /dev/null +++ b/doc/overview.rst @@ -0,0 +1,554 @@ + +============================================ +PyEpics Overview +============================================ + +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`, 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:: + + import epics + +The main components of this module include + + * 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 + use with Epics PVs. + +If you're looking to write quick scripts or a simple introduction to using +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 +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 +cases, this module does provide a fairly complete wrapping of the basic +EPICS CA library. For people who have used CA from C or other languages, +this module should be familiar and seem quite usable, if a little more +verbose and C-like than using PV objects. + +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 supported and well-tested on Linux, Mac OS X, and +Windows with Python versions 2.7, and 3.5 and above. + + +Quick Start +================= + +Whether you're familiar with Epics Channel Access or not, start here. +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. + +Procedural Approach: caget(), caput() +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To get values from PVs, you can use the :func:`caget` function: + + >>> 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 + +To see more detailed information about a PV, use the :func:`cainfo` +function: + + >>> 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 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') + +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 +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.90 + +or + + >>> print(pv1.value) + 1.90 + +To set a PV's value, you can either use + + >>> pv1.put(1.9) + +or assign the :attr:`value` attribute + + >>> pv1.value = 1.9 + +You can see a few of the most important properties of a PV by simply +printing it: + + >>> print(pv1) + + +More complete information can be seen by printing the PVs :attr:`info` +attribute:: + + >>> 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. +======================================================================== + +.. module:: epics + :synopsis: top-level epics module, and container for simplest CA functions + +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` +~~~~~~~~~~~~~ + +.. function:: caget(pvname[, as_string=False[, count=None[, as_numpy=True[, timeout=None[, use_monitor=False]]]]]) + + retrieves and returns the value of the named PV. + + :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. + :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`` + :param use_monitor: whether to rely on monitor callbacks or explicitly get value now. + :type use_monitor: ``True``/``False`` + +The *count* and *as_numpy* options apply only to array or waveform +data. The default behavior is to return the full data array and convert to +a numpy array if available. The *count* option can be used to explicitly +limit the number of array elements returned, and *as_numpy* can turn on or +off conversion to a numpy array. + +The *timeout* argument sets the maximum time to wait for a value to be +fetched over the network. If the timeout is exceeded, :func:`caget` will +return ``None``. This might imply that the PV is not actually available, +but it might also mean that the data is large or network slow enough that +the data just hasn't been received yet, but may show up later. + +The *use_monitor* argument sets whether to rely on the monitors from the +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 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 +depends on the variable type of the PV. For integer (short or long) and +string PVs, the string representation is pretty easy: 0 will become '0', +for example. For float and doubles, the internal precision of the PV is +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 + 0.10000000000000001 + + >>> print(caget('XXX:m1.DESC')) # A string PV + 'Motor 1' + >>> 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, 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)) + '0.10000' + + >>> print(caget('XXX:m1.FOFF', as_string=True)) + 'Frozen' + +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 + array([-0.08 , -0.078 , -0.076 , ..., + 1.99599814, 1.99799919, 2. ]) + + >>> print(caget('XXX:scan1.P1PA', as_string=True)) + '' + +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 which has +inspired the fairly common usage of CHAR waveforms to represent longer +strings:: + + >>> 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. + +:func:`caput` +~~~~~~~~~~~~~~~~ + +.. function:: caput(pvname, value[, wait=False[, timeout=60]]) + + set the value of the named PV. + + :param pvname: name of Epics Process Variable + :param value: value to send. + :param wait: whether to wait until the processing has completed. + :type wait: ``True``/``False`` + :param timeout: how long to wait (in seconds) for put to complete before giving up. + :type timeout: double + :rtype: integer + +The optional *wait* argument tells the function to wait until the +processing completes. This can be useful for PVs which take significant +time to complete, either because it causes a physical device (motor, valve, +etc) to move or because it triggers a complex calculation or data +processing sequence. The *timeout* argument gives the maximum time to +wait, in seconds. The function will return after this (approximate) time +even if the :func:`caput` has not completed. + +This function returns 1 on success, and a negative number if the timeout +has been exceeded. + + >>> from epics import caget, caput, cainfo + >>> caput('XXX:m1.VAL',2.30) + 1 + >>> caput('XXX:m1.VAL',-2.30, wait=True) + ... waits a few seconds ... + 1 + +:func:`cainfo` +~~~~~~~~~~~~~~ + +.. function:: cainfo(pvname[, print_out=True]) + + prints (or returns as a string) an informational paragraph about the PV, + including Control Settings. + + :param pvname: name of Epics Process Variable + :param print_out: whether to write results to standard output + (otherwise the string is returned). + :type print_out: ``True``/``False`` + + >>> from epics import caget, caput, cainfo + >>> cainfo('XXX.m1.VAL') + == XXX:m1.VAL (double) == + value = 2.3 + char_value = 2.3000 + count = 1 + units = mm + precision = 4 + host = xxx.aps.anl.gov:5064 + access = read/write + status = 1 + severity = 0 + timestamp = 1265996455.417 (2010-Feb-12 11:40:55.417) + upper_ctrl_limit = 200.0 + lower_ctrl_limit = -200.0 + upper_disp_limit = 200.0 + lower_disp_limit = -200.0 + upper_alarm_limit = 0.0 + lower_alarm_limit = 0.0 + upper_warning_limit = 0.0 + lower_warning = 0.0 + PV is monitored internally + no user callbacks defined. + ============================= + +:func:`camonitor` +~~~~~~~~~~~~~~~~~ + + +.. function:: camonitor(pvname[, writer=None[, callback=None]]) + + This `sets a monitor` on the named PV, which will cause *something* to be + done each time the value changes. By default the PV name, time, and + value will be printed out (to standard output) when the value changes, + but the action that actually happens can be customized. + + :param pvname: name of Epics Process Variable + :param writer: where to write results to standard output . + :type writer: ``None`` or a callable function that takes a string argument. + :param callback: user-supplied function to receive result + :type callback: ``None`` or callable function + +One can specify any function that can take a string as *writer*, such as +the :meth:`write` method of an open file that has been open for writing. +If left as ``None``, messages of changes will be sent to +:func:`sys.stdout.write`. For more complete control, one can specify a +*callback* function to be called on each change event. This callback +should take keyword arguments for *pvname*, *value*, and *char_value*. See +:ref:`pv-callbacks-label` for information on writing callback functions for +:func:`camonitor`. + + >>> from epics import camonitor + >>> camonitor('XXX.m1.VAL') + XXX.m1.VAL 2010-08-01 10:34:15.822452 1.3 + XXX.m1.VAL 2010-08-01 10:34:16.823233 1.2 + XXX.m1.VAL 2010-08-01 10:34:17.823233 1.1 + XXX.m1.VAL 2010-08-01 10:34:18.823233 1.0 + + +:func:`camonitor_clear` +~~~~~~~~~~~~~~~~~~~~~~~ + +.. function:: camonitor_clear(pvname) + + clears a monitor set on the named PV by :func:`camonitor`. + + :param pvname: name of Epics Process Variable + +This simple example monitors a PV with :func:`camonitor` for while, with +changes being saved to a log file. After a while, the monitor is cleared +and the log file is inspected:: + + >>> import epics + >>> fh = open('PV1.log','w') + >>> epics.camonitor('XXX:DMM1Ch2_calc.VAL',writer=fh.write) + >>> .... wait for changes ... + >>> epics.camonitor_clear('XXX:DMM1Ch2_calc.VAL') + >>> fh.close() + >>> fh = open('PV1.log','r') + >>> 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 + XXX:DMM1Ch2_calc.VAL 2010-03-24 11:56:43.535379 -183.5466 + XXX:DMM1Ch2_calc.VAL 2010-03-24 11:56:44.535191 -183.4890 + XXX:DMM1Ch2_calc.VAL 2010-03-24 11:56:45.535001 -183.5066 + XXX:DMM1Ch2_calc.VAL 2010-03-24 11:56:46.535813 -183.5085 + 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]]]]) + + 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 +================================================ + +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) support for both Python 2 and Python 3. + 5) using Python's ctypes library. + +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 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.7 and Python 3 simultaneously. + +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. + + * build and improve applications using PyEpics, especially for common data + acquisition needs. + + * improve and extend the use of PyQt widgets with PyEpics. + +If you are interested in working on any of these or other topics, please +contact the authors. diff --git a/doc/pv.rst b/doc/pv.rst new file mode 100644 index 0000000..2c0d47d --- /dev/null +++ b/doc/pv.rst @@ -0,0 +1,1010 @@ +.. _pv-label: + +============================== +PV: Epics Process Variables +============================== + + +.. module:: pv + :synopsis: PV objects for Epics Channel Access + +The :mod:`pv` module provides a higher-level class :class:`pv.PV`, which +creates a `PV` object for an EPICS Process Variable. A `PV` object has +both methods and attributes for accessing it's properties. + + +The :class:`PV` class +======================= + +.. class:: PV(pvname[, callback=None[, form='time'[, verbose=False[, auto_monitor=None[, count=None[, connection_callback=None[, connection_timeout=None[, access_callback=None]]]]]]]] ) + create a PV object for a named Epics Process Variable. + + :param pvname: name of Epics Process Variable + :param callback: user-defined callback function on changes to PV value or state. + :type callback: callable, tuple, list or None + :param form: which epics *data type* to use: the 'native', 'time', or the 'ctrl' (Control) variant. + :type form: string, one of ('native','ctrl', or 'time') + :param verbose: whether to print out debugging messages + :type verbose: ``True``/``False`` + :param auto_monitor: whether to automatically monitor the PV for changes. + :type auto_monitor: ``None``, ``True``, ``False``, or bitmask (see :ref:`pv-automonitor-label`) + :param count: number of data elements to return by default (see :ref:`here `) + :type count: int + :param connection_callback: user-defined function called on changes to PV connection status. + :type connection_callback: callable or ``None`` + :param connection_timeout: time (in seconds) to wait for connection before giving up + :type connection_timeout: float or ``None`` + :param access_callback: user-defined function called on changes to PV access rights + :type access_callback: callable or ``None`` + +Once created, a PV should (barring any network issues) automatically +connect and be ready to use. + + >>> from epics import PV + >>> p = PV('XX:m1.VAL') + >>> print p.get() + >>> print p.count, p.type + + +The *pvname* is required, and is the name of an existing Process Variable. + +The *callback* parameter specifies one or more python methods to be called +on changes, as discussed in more detail at :ref:`pv-callbacks-label` + +The *connection_callback* parameter specifies a python method to be called +on changes to the connection status of the PV (that is, when it connects or +disconnects). This is discussed in more detail at :ref:`pv-connection_callbacks-label` + +The *form* parameter specifies which of the three variants 'native', 'ctrl' +(Control) or 'time' (the default) to use for the PV. The 'native' form +returns just the value, the 'time' form includes the timestamp from the +server the PV lives on, as well as status information. The control form +includes several additional fields such as limits to the PV, which can be +useful in some cases. Also note that the additional 'ctrl' value fields +(see the :ref:`Table of Control Attributes `) can be +obtained with :meth:`get_ctrlvars` even for PVs of 'native' or 'time' form. + +The *auto_monitor* parameter specifies whether the PV should be +automatically monitored. See :ref:`pv-automonitor-label` for a detailed +description of this. + +The *verbose* parameter specifies more verbose output on changes, and is +intended for debugging purposes. + +The *access_callback* parameter specifies a python method to be called on +changes to the access rights of the PV (read/write access changes). This +is discussed in more detail :ref:`here `. + + + +methods +~~~~~~~~ + +A `PV` has several methods for getting and setting its value and defining +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, [with_ctrlvars=False]]]]]]) + + get and return the current value of the PV + + :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 :ref:`pv-as-string-label` for details on how the string + representation is determined. + + With the *as_numpy* option, an array PV (that is, a PV whose value has + more than one element) will be returned as a numpy array, provided the + numpy module is available. See :ref:`arrays-large-label` for a + discussion of strategies for how to best deal with very large arrays. + + The *use_monitor* option controls whether the most recent value from the automatic + monitoring will be used or whether the value will be explicitly asked + for right now. Usually, you can rely on a PVs value being kept up to + date, and so the default here is ``True``. But, since network traffic + is not instantaneous and hard to predict, the value returned with + `use_monitor=True` may be out-of-date. + + The *timeout* sets how long (in seconds) to wait for the value to be + sent. This only applies with `use_monitor=False`, or if the PV is not + 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 + completed, or setting the :attr:`put_complete` to indicate complete-ness. + + :param value: value to set PV + :param wait: whether to wait for processing to complete (or time-out) before returning. + :type wait: ``True``/``False`` + :param timeout: maximum time to wait for processing to complete before returning anyway. + :type timeout: float + :param use_complete: whether to use a built-in callback to set :attr:`put_complete`. + :type use_complete: ``True``/``False`` + :param callback: user-supplied function to run when processing has completed. + :type callback: ``None`` or a valid python function + :param callback_data: extra data to pass on to a user-supplied callback function. + +The `wait` and `callback` arguments, as well as the 'use_complete' / :attr:`put_complete` +attribute give a few options for knowing that a :meth:`put` has +completed. See :ref:`pv-putwait-label` for more details. + +.. _pv-get-ctrlvars-label: + +.. method:: get_ctrlvars() + + returns a dictionary of the **control values** for the PV. This + dictionary may have many members, depending on the data type of PV. See + the :ref:`Table of Control Attributes ` for details. + +.. method:: get_timevars() + + returns a dictionary of the **time values** for the PV, which + include `status`, `severity`, and the `timestamp` from the CA + server. + +.. method:: poll([evt=1.e-4, [iot=1.0]]) + + poll for changes. This simply calls :meth:`epics.ca.poll` + + :param evt: time to pass to :meth:`epics.ca.pend_event` + :type evt: float + :param iot: time to pass to :meth:`epics.ca.pend_io` + :type iot: float + +.. method:: connect([timeout=None]) + + this explicitly connects a PV, and returns whether or not it has + 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:`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:`episc.ca.DEFAULT_CONNECTION_TIMEOUT` will be used. + +.. method:: wait_for_connection([timeout=None]) + + this waits until a PV is connected, or has timed-out waiting for a + connection. Returns whether the connection has occurred. + + :param timeout: maximum connection time. + :type timeout: float + :rtype: ``True``/``False`` + + if timeout is ``None``, the PVs connection_timeout parameter will be used. If that is also ``None``, + :data:`epics.ca.DEFAULT_CONNECTION_TIMEOUT` will be used. + +.. method:: disconnect() + + disconnect a PV, clearing all callbacks. + +.. method:: reconnect() + + reconnect (or try to) a disconnected PV. + +.. method:: clear_auto_monitor() + + 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:`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. + +.. method:: add_callback(callback=None[, index=None [, with_ctrlvars=True[, **kw]]) + + adds a user-defined callback routine to be run on each change event for + this PV. Returns the integer *index* for the callback. + + :param callback: user-supplied function to run when PV changes. + :type callback: ``None`` or callable + :param index: identifying key for this callback + :param with_ctrlvars: whether to (try to) make sure that accurate ``control values`` will be sent to the callback. + :type index: ``None`` (integer will be produced) or immutable + :param kw: additional keyword/value arguments to pass to each execution of the callback. + :rtype: integer + + Note that multiple callbacks can be defined, each having its own index + (a dictionary key, typically an integer). When a PV changes, all the + defined callbacks will be executed. They will be called in order (by + sorting the keys of the :attr:`callbacks` dictionary) + + See also: :attr:`callbacks` attribute, :ref:`pv-callbacks-label` + +.. method:: remove_callback(index=None) + + remove a user-defined callback routine using supplied + + :param index: index of user-supplied function, as returned by :meth:`add_callback`, + and also to key for this callback in the :attr:`callbacks` dictionary. + :type index: ``None`` or integer + :rtype: integer + + If only one callback is defined an index=``None``, this will clear the + only defined callback. + + See also: :attr:`callbacks` attribute, :ref:`pv-callbacks-label` + +.. method:: clear_callbacks() + + remove all user-defined callback routine. + +.. method:: run_callbacks() + + execute all user-defined callbacks right now, even if the PV has not + changed. Useful for debugging! + + See also: :attr:`callbacks` attribute, :ref:`pv-callbacks-label` + +.. method:: run_callback(index) + + execute a particular user-defined callback right now, even if the PV + has not changed. Useful for debugging! + + See also: :attr:`callbacks` attribute, :ref:`pv-callbacks-label` + +.. method:: force_read_access_rights() + + force a read of the access rights for a PV. Normally, a PV will + have access rights determined automatically and subscribe to + changes in access rights. But sometimes (especially 64-bit + Windows), the automatically reported values are wrong. This + methods will explicitly read the access rights. + +attributes +~~~~~~~~~~ + +A PV object has many attributes, each associated with some property of the +underlying PV: its *value*, *host*, *count*, and so on. For properties +that can change, the PV attribute will hold the latest value for the +corresponding property, Most attributes are **read-only**, and cannot be +assigned to. The exception to this rule is the :attr:`value` attribute. + +.. attribute:: value + + The current value of the PV. + + **Note**: The :attr:`value` attribute can be assigned to. + When read, the latest value will be returned, even if that means a + :meth:`get` needs to be called. + + Assigning to :attr:`value` is equivalent to setting the value with the + :meth:`put` method. + + >>> from epics import PV + >>> p1 = PV('xxx.VAL') + >>> print p1.value + 1.00 + >>> p1.value = 2.00 + +.. attribute:: char_value + + The string representation of the string, as described in :meth:`get`. + +.. attribute:: status + + The PV status, which will be 1 for a Normal, connected PV. + +.. attribute:: type + + string describing data type of PV, such as `double`, `float`, `enum`, `string`, + `int`, `long`, `char`, or one of the `ctrl` or `time` variants of these, which + will be named `ctrl_double`, `time_enum`, and so on. See the + :ref:`Table of DBR Types ` + + +.. attribute:: ftype + + The integer value (from the underlying C library) indicating the PV data + type according to :ref:`Table of DBR Types ` + +.. attribute:: host + + string of host machine provide this PV. + +.. attribute:: count + + number of data elements in a PV. 1 except for waveform PVs, where it + gives the number of elements in the waveform. For recent versions of + Epics Base (3.14.11 and later?), this gives the `.NORD` field, which + gives the number of elements last put into the PV and which may be less + than the maximum number allowed (see `nelm` below). + +.. attribute:: nelm + + number of data elements in a PV. 1 except for waveform PVs where it + gives the maximum number of elements in the waveform. For recent + versions of Epics Base (3.14.11 and later?), this gives the `.NELM` + parameter. See also the `count` attribute above. + +.. attribute:: read_access + + Boolean (``True``/``False``) for whether PV is readable + +.. attribute:: write_access + + Boolean (``True``/``False``) for whether PV is writable + +.. attribute:: access + + string describing read/write access. One of + 'read/write','read-only','write-only', 'no access'. + +.. attribute:: severity + + severity value of PV. Usually 0 for PVs that are not in an alarm + condition. + +.. attribute:: timestamp + + floating point timestamp (relative to the POSIX time origin, not the + EPICS time origin) of the last event seen for this PV. Note that this + is will contain the timestamp from the Epics server if the PV object was + created with the ``form='time'`` option. Otherwise, the timestamp will + be set to time according to the client, indicating when the data arrive + from the server. + +.. attribute:: posixseconds + + Integer number of seconds (relative to the POSIX time origin, not the + EPICS time origin) of the last event seen for this PV. This will be set + only if the PV object was created with the ``form='time'`` option, and + will reflect the timestamp from the server. Otherwise, this value will + be 0 which can be used to signal that the `timestamp` attribute is from + the client. + +.. attribute:: nanoseconds + + Integer number of nanoseconds for the last event seen for this PV. This + will be set only if the PV object was created with the ``form='time'`` + option, and will give higher time resolution than the `timestamp` + attribute. + +.. attribute:: precision + + number of decimal places of precision to use for float and double PVs + +.. attribute:: units + + string of engineering units for PV + +.. attribute:: enum_strs + + a list of strings for the enumeration states of this PV (for enum PVs) + +.. attribute:: info + + a string paragraph (ie, including newlines) showing much of the + information about the PV. + +.. attribute:: upper_disp_limit + +.. attribute:: lower_disp_limit + +.. attribute:: upper_alarm_limit + +.. attribute:: lower_alarm_limit + +.. attribute:: lower_warning_limit + +.. attribute:: upper_warning_limit + +.. attribute:: upper_ctrl_limit + +.. attribute:: lower_ctrl_limit + + These are all the various kinds of limits for a PV. + +.. attribute:: put_complete + + a Boolean (``True``/``False``) value for whether the most recent + :meth:`put` has completed. + +.. attribute:: callbacks + + a dictionary of currently defined callbacks, to be run on changes to the + PV. This dictionary has integer keys (generally in increasing order of + when they were defined) which sets which order for executing the + callbacks. The values of this dictionary are tuples of `(callback, + keyword_arguments)`. + + **Note**: The :attr:`callbacks` attribute can be assigned to or + manipulated directly. This is not recommended. Use the + methods :meth:`add_callback`, :meth:`remove_callback`, and + :meth:`clear_callbacks` instead of altering this dictionary directly. + +.. attribute:: connection_callbacks + + a simple list of connection callbacks: functions to be run when the + connection status of the PV changes. See + :ref:`pv-connection_callbacks-label` for more details. + +.. attribute:: access_callbacks + + an :attr:`list` of access callbacks: functions to be run when the + access rights of the PV changes. See + :ref:`pv-access-rights-callback-label` for more details. + +.. _pv-as-string-label: + +String representation for a PV +================================ + +The string representation for a `PV`, as returned either with the +*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`), +will depend on the native type and count of a `PV`. +:ref:`Table of String Representations ` + +.. _charvalue_table: + + Table of String Representations: How raw data :attr:`value` is mapped + to :attr:`char_value` for different native data types. + + =============== ========== ============================== + *data types* *count* *char_value* + =============== ========== ============================== + string 1 = value + char 1 = value + short 1 = str(value) + long 1 = str(value) + enum 1 = enum_str[value] + double 1 = ("%%.%if" % (precision)) % value + float 1 = ("%%.%if" % (precision)) % value + char > 1 = long string from bytes in array + all others > 1 = + =============== ========== ============================== + +For double/float values with large exponents, the formatting will be +`("%%.%ig" % (precision)) % value`. For character waveforms (*char* data +with *count* > 1), the :attr:`char_value` will be set according to:: + + >>> firstnull = val.index(0) + >>> if firstnull == -1: firstnull= len(val) + >>> char_value = ''.join([chr(i) for i in val[:firstnull]).rstrip() + +.. _pv-automonitor-label: + +Automatic Monitoring of a PV +================================ + +When creating a PV, the *auto_monitor* parameter specifies whether the PV +should be automatically monitored or not. Automatic monitoring means that +an internal callback will be registered for changes. Any callbacks defined +by the user will be called by this internal callback when changes occur. + +For most scalar-value PVs, this automatic monitoring is desirable, as the +PV will see all changes (and run callbacks) without any additional +interaction from the user. The PV's value will always be up-to-date and no +unnecessary network traffic is needed. + +Possible values for :attr:`auto_monitor` are: + +``False`` + For some PVs, especially those that change much more rapidly than you care + about or those that contain large arrays as values, auto_monitoring can add + network traffic that you don't need. For these, you may wish to create + your PVs with *auto_monitor=False*. When you do this, you will need to + make calls to :meth:`get` to explicitly get the latest value. + +``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:`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 + *auto_monitor=True*, or to an explicit mask. See + :ref:`arrays-large-label` for more details. + +``True`` + When *auto_monitor* is set to ``True``, the value will be monitored using + 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 + than the monitor deadband, or when the PV alarm status changes. This + behavior is the same as the default in EPICS' *camonitor* tool. + +*Mask* + 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:`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) + pv3 = PV('CCC', auto_monitor=dbr.DBE_VALUE|dbr.DBE_ALARM|dbr.DBE_LOG) + + which will generate callbacks for pv1 only when the value of 'AAA' + changes, while pv2 will receive callbacks if the value or alarm state of + 'BBB' changes, and pv3 will receive callbacks for all changes to 'CCC'. + Note that these dbr.DBE_**** constants are ORed together as a bitmask. + +.. _pv-callbacks-label: + +User-supplied Callback functions +================================ + +This section describes user-defined functions that are called when the +value of a PV changes. These callback functions are useful as they allow +you to be notified of changes without having to continually ask for a PVs +current value. Much of this information is similar to that in +:ref:`ca-callbacks-label` for the :mod:`ca` module, though there are some +important enhancements to callbacks on `PV` objects. + +You can define more than one callback function per PV to be run on value +changes. These functions can be specified when creating a PV, with the +*callback* argument which can take either a single callback function or a +list or tuple of callback functions. After a PV has been created, you can +add callback functions with :meth:`add_callback`, remove them with +:meth:`remove_callback`, and explicitly run them with :meth:`run_callback`. +Each callback has an internal unique *index* (a small integer number) that +can be used for specifying which one to add, remove, and run. + +When defining a callback function to be run on changes to a PV, it is +important to know two things: + + 1) how your function will be called. + 2) what is permissible to do inside your callback function. + +Callback functions will be called with several keyword arguments. You +should be prepared to have them passed to your function, and should always +include `**kw` to catch all arguments. Your callback will be sent the +following keyword parameters: + + * `pvname`: the name of the pv + * `value`: the latest value + * `char_value`: string representation of value + * `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) + * `precision`: number of decimal places of precision for floating point values + * `units`: string for PV units + * `severity`: PV severity + * `timestamp`: timestamp from CA server. + * `read_access`: read access (``True``/``False``) + * `write_access`: write access (``True``/``False``) + * `access`: string description of read- and write-access + * `host`: host machine and CA port serving PV + * `enum_strs`: the list of enumeration strings + * `upper_disp_limit`: upper display limit + * `lower_disp_limit`: lower display limit + * `upper_alarm_limit`: upper alarm limit + * `lower_alarm_limit`: lower alarm limit + * `upper_warning_limit`: upper warning limit + * `lower_warning_limit`: lower warning limit + * `upper_ctrl_limit`: upper control limit + * `lower_ctrl_limit`: lower control limit + * `chid`: integer channel ID + * `cb_info`: (index, self) tuple containing callback ID + and the PV object + +Some of these may not be directly applicable to all PV data types, and some +values may be ``None`` if the control parameters have not yet been fetched with +:meth:`get_ctrlvars`. + +It is important to keep in mind that the callback function will be run +*inside* a CA function, and cannot reliably make any other CA calls. It is +helpful to think "this all happens inside of a :func:`pend_event` call", +and in an epics thread that may or may not be the main thread of your +program. It is advisable to keep the callback functions short and not +resource-intensive. Consider strategies which use the callback only to +record that a change has occurred and then act on that change later -- +perhaps in a separate thread, perhaps after :func:`pend_event` has +completed. + +The `cb_info` parameter supplied to the callback needs special attention, +as it is the only non-Epics information passed. The `cb_info` parameter +will be a tuple containing (:attr:`index`, :attr:`self`) where +:attr:`index` is the key for the :attr:`callbacks` dictionary for the PV +and :attr:`self` *is* PV object. A principle use of this tuple is to +**remove the current callback** if an error happens, as for example in GUI +code if the widget that the callback is meant to update disappears. + +.. _pv-connection_callbacks-label: + +User-supplied Connection Callback functions +============================================= + +A *connection* callback is a user-defined function that is called when the +connection status of a PV changes -- that is, when a PV initially +connects, disconnects or reconnects due to the process serving the PV going +away, or loss of network connection. A connection callback can be +specified when a PV is created, or can be added by appending to the +:attr:`connection_callbacks` list. If there is more than one connection +callback defined, they will all be run when the connection state changes. + +A connection callback should be prepared to receive the following keyword arguments: + + * `pvname`: the name of the pv + * `conn`: the connection status + +where *conn* will be either ``True` or ``False``, specifying whether the PV is +now connected. A simple example is given below. + +.. _pv-access-rights-callback-label: + +User-supplied Access Rights Callback functions +=============================================== + +An *access rights* callback is a user-defined function that is called when the +access rights - read/write permissions - of a PV undergo changes. The callback +will be invoked upon successful initialization and at all events that change +a PV's access rights, including disconnection and reconnection events. +An *access rights* callback can be specified when a PV is created, or can be +added by appending to the :attr:`access_callbacks` list of the PV object. +If there are multiple access rights callbacks defined for a PV, they will all +be run on access rights events. + +.. _pv-putwait-label: + +Put with wait, put callbacks, and put_complete +======================================================== + +Some EPICS records take a significant amount of time to fully process, and +sometimes you want to wait until the processing completes before going on. +There are a few ways to accomplish this. First, one can simply wait until +the processing is done:: + + import epics + p = epics.PV('XXX') + p.put(1.0, wait=True) + print 'Done' + +This will hang until the processing of the PV completes (motor moving, etc) +before printing 'Done'. You can also specify a maximum time to wait -- a +*timeout* (in seconds):: + + p.put(1.0, wait=True, timeout=30) + +which will wait up to 30 seconds. For the pedantic, this timeout should +not be used as an accurate clock -- the actual wait time may be slightly +longer. + +A second method is to use the 'use_complete' option and watch for the +:attr:`put_complete` attribute to become ``True`` after a :meth:`put`. This is +somewhat more flexible than using `wait=True` as above, because you can more +carefully control how often you look for a :meth:`put` to complete, and +what to do in the interim. A simple example would be:: + + p.put(1.0, use_complete=True) + waiting = True + while waiting: + time.sleep(0.001) + waiting = not p.put_complete + +An additional advantage of this approach is that you can easily wait for +multiple PVs to complete with python's built-in *all* function, as with:: + + pvgroup = (epics.PV('XXX'), epics.PV('YYY'), epics.PV('ZZZ')) + newvals = (1.0, 2.0, 3.0) + for pv, val in zip(pvgroup, newvals): + pv.put(val, use_complete=True) + + waiting = True + while waiting: + time.sleep(0.001) + waiting = not all([pv.put_complete for pv in pvgroup]) + print 'All puts are done!' + +For maximum flexibility, one can all define a *put callback*, a function to +be run when the :meth:`put` has completed. This function requires a +*pvname* keyword argument, but will receive no others, unless you pass in +data with the *callback_data* argument (which should be dict-like) to +:meth:`put`. A simple example would be:: + + pv = epics.PV('XXX') + def onPutComplete(pvname=None, **kws): + print 'Put done for %s' % pvname + + pv.put(1.0, callback=onPutComplete) + +.. _pv-cache-label: + +The :func:`get_pv` function and :attr:`_PVcache_` cache of PVs +============================================================================ + +As mentioned in the previous chapter, a cache of PVs is maintained for each +process using pyepics. When using :func:`epics.caget`, :func:`epics.caput` +and so forth, or when creating a :class:`PV` directly, the corresponding PV +is kept in a global cache, held in :attr:`pv._PVcache_`. + +The function :func:`get_pv` will retrieve the named PV from this cache, or +create a new :class:`PV` if one is not found. In long-running or complex +processes, it is not unusual to access a particular PV many times, perhaps +calling a function that creates a PV but only keeping that PV object for +the life of the function. Using :func:`get_pv` instead of creating a +:class:`PV` can improve performance (the PV is already connected) and is +highly recommended. + +.. function:: get_pv(pvname[, form='time'[, connect=False[, timeout=5[, context=None[, **kws]]]]]) + + retrieves a PV from :attr:`_PVcache` or creates and returns a new PV. + + :param pvname: name of Epics Process Variable + :param form: which epics *data type* to use: the 'native' , or the 'ctrl' (Control) or 'time' variant. + :type form: string, one of ('native','ctrl', or 'time') + :param connect: whether to wait for the PV to connect. + :type connect: ``True``/``False`` + :param timeout: maximum time to wait (in seconds) for value before returning None. + :type timeout: float or ``None`` + :param context: integer threading context. + :type context: integer or ``None`` (default) + + + Additional keywords are passed directly to :class:`PV`. + +.. attribute:: _PVcache_ + + A cache of :class:`PV` objects for the process. + +.. _pv-examples-label: + +Examples +============ + +Some simple examples using PVs follow. + +Basic Use +~~~~~~~~~~~~ + +The simplest approach is to simply create a PV and use its :attr:`value` +attribute: + + >>> from epics import PV + >>> p1 = PV('xxx.VAL') + >>> print p1.value + 1.00 + >>> p1.value = 2.00 + +The *print p1.value* line automatically fetches the current PV value. The +*p1.value = 2.00* line does a :func:`put` to set the value, causing any +necessary processing over the network. + +The above example is equivalent to + + >>> from epics import PV + >>> p1 = PV('xxx.VAL') + >>> print p1.get() + 1.00 + >>> p1.put(value = 2.00) + +To get a string representation of the value, you can use either + + >>> print p1.get(as_string=True) + '1.000' + +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 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A PV has many attributes. This can be seen from its *info* paragraph: + +>>> import epics +>>> p = epics.PV('13IDA:m3') +>>> print p.info +== 13IDA:m3 (native_double) == + value = 0.2 + char_value = '0.200' + count = 1 + type = double + units = mm + precision = 3 + host = ioc13ida.cars.aps.anl.gov:5064 + access = read/write + status = 0 + severity = 0 + timestamp = 1274809682.967 (2010-05-25 12:48:02.967364) + upper_ctrl_limit = 5.49393415451 + lower_ctrl_limit = -14.5060658455 + upper_disp_limit = 5.49393415451 + lower_disp_limit = -14.5060658455 + 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 individual attributes can also be accessed as below. Many of these +(the *control attributes*, see :ref:`Table of Control Attributes +`) will not be filled in until either the :attr:`info` +attribute is accessed or until :meth:`get_ctrlvars` is called. + +>>> print p.type +double +>>> print p.units, p.precision, p.lower_disp_limit +mm 3 -14.5060658455 + + +Getting a string value +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +It is not uncommon to want a string representation of a PVs value, for +example to show in a display window or to write to some report. For string +PVs and integer PVs, this is a simple task. For floating point values, +there is ambiguity how many significant digits to show. EPICS PVs all have +a :attr:`precision` field. which sets how many digits after the decimal +place should be described. In addition, for ENUM PVs, it would be +desire able to get at the name of the ENUM state, not just its integer +value. + +To get the string representation of a PVs value, use either the +:attr:`char_value` attribute or the `as_string=True` argument to :meth:`get` + + +Example of :meth:`put` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To put a new value to a variable, either of these two approaches can be +used: + +>>> import epics +>>> p = epics.PV('XXX') +>>> p.put(1.0) + +Or (equivalently): + +>>> import epics +>>> p = epics.PV('XXX') +>>> p.value = 1.0 + +The :attr:`value` attribute is the only attribute that can be set. + + +Example of simple callback +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +It is often useful to get a notification of when a PV changes. In general, +it would be inconvenient (and possibly inefficient) to have to continually +ask if a PVs value has changed. Instead, it is better to set a *callback* +function: a function to be run when the value has changed. + +A simple example of this would be:: + + import epics + import time + def onChanges(pvname=None, value=None, char_value=None, **kw): + print 'PV Changed! ', pvname, char_value, time.ctime() + + + mypv = epics.PV(pvname) + mypv.add_callback(onChanges) + + print 'Now wait for changes' + + t0 = time.time() + while time.time() - t0 < 60.0: + time.sleep(1.e-3) + print 'Done.' + +This first defines a *callback function* called `onChanges()` and then +simply waits for changes to happen. Note that the callback function should +take keyword arguments, and generally use `**kw` to catch all arguments. +See :ref:`pv-callbacks-label` for more details. + +Example of connection callback +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A connection callback: + +.. literalinclude:: ../tests/pv_connection_callback.py + +Example of an access rights callback +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Associating an access rights callback with a PV:: + + import epics + import time + + def access_rights_callback(read_access, write_access, pv=None): + print "%s - read=%s, write=%s" % (pv.pvname, read_access, write_access) + + # should immediately see the message upon connection + apv = epics.PV('pvname', access_callback=access_rights_callback) + + try: + start = time.time() + while (time.time() - start) < 30: + time.sleep(0.25) + except KeyboardInterrupt: + pass diff --git a/doc/sphinx/theme/epicsdoc/layout.html b/doc/sphinx/theme/epicsdoc/layout.html new file mode 100644 index 0000000..c931918 --- /dev/null +++ b/doc/sphinx/theme/epicsdoc/layout.html @@ -0,0 +1,14 @@ +{# + sphinxdoc/layout.html + ~~~~~~~~~~~~~~~~~~~~~ + + Sphinx layout template for the sphinxdoc theme. + + :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +#} +{% extends "basic/layout.html" %} + +{# put the sidebar before the body #} +{% block sidebar1 %}{{ sidebar() }}{% endblock %} +{% block sidebar2 %}{% endblock %} diff --git a/doc/sphinx/theme/epicsdoc/static/basic.css_t b/doc/sphinx/theme/epicsdoc/static/basic.css_t new file mode 100644 index 0000000..2937fa4 --- /dev/null +++ b/doc/sphinx/theme/epicsdoc/static/basic.css_t @@ -0,0 +1,540 @@ +/* + * basic.css + * ~~~~~~~~~ + * + * Sphinx stylesheet -- basic theme. + * + * :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +/* -- main layout ----------------------------------------------------------- */ + +div.clearer { + clear: both; +} + +/* -- relbar ---------------------------------------------------------------- */ + +div.related { + width: 100%; + font-size: 90%; +} + +div.related h3 { + display: none; +} + +div.related ul { + margin: 0; + padding: 0 0 0 10px; + list-style: none; +} + +div.related li { + display: inline; +} + +div.related li.right { + float: right; + margin-right: 5px; +} + +/* -- sidebar --------------------------------------------------------------- */ + +div.sphinxsidebarwrapper { + padding: 10px 5px 0 10px; +} + +div.sphinxsidebar { + float: left; + width: {{ theme_sidebarwidth|toint }}px; + margin-left: -100%; + font-size: 90%; +} + +div.sphinxsidebar ul { + list-style: none; +} + +div.sphinxsidebar ul ul, +div.sphinxsidebar ul.want-points { + margin-left: 20px; + list-style: square; +} + +div.sphinxsidebar ul ul { + margin-top: 0; + margin-bottom: 0; +} + +div.sphinxsidebar form { + margin-top: 10px; +} + +div.sphinxsidebar input { + border: 1px solid #98dbcc; + font-family: sans-serif; + font-size: 1em; +} + +div.sphinxsidebar #searchbox input[type="text"] { + width: 170px; +} + +div.sphinxsidebar #searchbox input[type="submit"] { + width: 30px; +} + +img { + border: 0; +} + +/* -- search page ----------------------------------------------------------- */ + +ul.search { + margin: 10px 0 0 20px; + padding: 0; +} + +ul.search li { + padding: 5px 0 5px 20px; + background-image: url(file.png); + background-repeat: no-repeat; + background-position: 0 7px; +} + +ul.search li a { + font-weight: bold; +} + +ul.search li div.context { + color: #888; + margin: 2px 0 0 30px; + text-align: left; +} + +ul.keywordmatches li.goodmatch a { + font-weight: bold; +} + +/* -- index page ------------------------------------------------------------ */ + +table.contentstable { + width: 90%; +} + +table.contentstable p.biglink { + line-height: 150%; +} + +a.biglink { + font-size: 1.3em; +} + +span.linkdescr { + font-style: italic; + padding-top: 5px; + font-size: 90%; +} + +/* -- general index --------------------------------------------------------- */ + +table.indextable { + width: 100%; +} + +table.indextable td { + text-align: left; + vertical-align: top; +} + +table.indextable dl, table.indextable dd { + margin-top: 0; + margin-bottom: 0; +} + +table.indextable tr.pcap { + height: 10px; +} + +table.indextable tr.cap { + margin-top: 10px; + background-color: #f2f2f2; +} + +img.toggler { + margin-right: 3px; + margin-top: 3px; + cursor: pointer; +} + +div.modindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +div.genindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +/* -- general body styles --------------------------------------------------- */ + +a.headerlink { + visibility: hidden; +} + +h1:hover > a.headerlink, +h2:hover > a.headerlink, +h3:hover > a.headerlink, +h4:hover > a.headerlink, +h5:hover > a.headerlink, +h6:hover > a.headerlink, +dt:hover > a.headerlink { + visibility: visible; +} + +div.body p.caption { + text-align: inherit; +} + +div.body td { + text-align: left; +} + +.field-list ul { + padding-left: 1em; +} + +.first { + margin-top: 0 !important; +} + +p.rubric { + margin-top: 30px; + font-weight: bold; +} + +img.align-left, .figure.align-left, object.align-left { + clear: left; + float: left; + margin-right: 1em; +} + +img.align-right, .figure.align-right, object.align-right { + clear: right; + float: right; + margin-left: 1em; +} + +img.align-center, .figure.align-center, object.align-center { + display: block; + margin-left: auto; + margin-right: auto; +} + +.align-left { + text-align: left; +} + +.align-center { + text-align: center; +} + +.align-right { + text-align: right; +} + +/* -- sidebars -------------------------------------------------------------- */ + +div.sidebar { + margin: 0 0 0.5em 1em; + border: 1px solid #ddb; + padding: 7px 7px 0 7px; + background-color: #ffe; + width: 40%; + float: right; +} + +p.sidebar-title { + font-weight: bold; +} + +/* -- topics ---------------------------------------------------------------- */ + +div.topic { + border: 1px solid #ccc; + padding: 7px 7px 0 7px; + margin: 10px 0 10px 0; +} + +p.topic-title { + font-size: 1.1em; + font-weight: bold; + margin-top: 10px; +} + +/* -- admonitions ----------------------------------------------------------- */ + +div.admonition { + margin-top: 10px; + margin-bottom: 10px; + padding: 7px; +} + +div.admonition dt { + font-weight: bold; +} + +div.admonition dl { + margin-bottom: 0; +} + +p.admonition-title { + margin: 0px 10px 5px 0px; + font-weight: bold; +} + +div.body p.centered { + text-align: center; + margin-top: 25px; +} + +/* -- tables ---------------------------------------------------------------- */ + +table.docutils { + border: 0; + border-collapse: collapse; +} + +table.docutils td, table.docutils th { + padding: 1px 8px 1px 5px; + border-top: 0; + border-left: 0; + border-right: 0; + border-bottom: 1px solid #aaa; +} + +table.field-list td, table.field-list th { + border: 0 !important; +} + +table.footnote td, table.footnote th { + border: 0 !important; +} + +th { + text-align: left; + padding-right: 5px; +} + +table.citation { + border-left: solid 1px gray; + margin-left: 1px; +} + +table.citation td { + border-bottom: none; +} + +/* -- other body styles ----------------------------------------------------- */ + +ol.arabic { + list-style: decimal; +} + +ol.loweralpha { + list-style: lower-alpha; +} + +ol.upperalpha { + list-style: upper-alpha; +} + +ol.lowerroman { + list-style: lower-roman; +} + +ol.upperroman { + list-style: upper-roman; +} + +dl { + margin-bottom: 15px; +} + +dd p { + margin-top: 0px; +} + +dd ul, dd table { + margin-bottom: 10px; +} + +dd { + margin-top: 3px; + margin-bottom: 10px; + margin-left: 30px; +} + +dt:target, .highlighted { + background-color: #fbe54e; +} + +dl.glossary dt { + font-weight: bold; + font-size: 1.1em; +} + +.field-list ul { + margin: 0; + padding-left: 1em; +} + +.field-list p { + margin: 0; +} + +.refcount { + color: #060; +} + +.optional { + font-size: 1.3em; +} + +.versionmodified { + font-style: italic; +} + +.system-message { + background-color: #fda; + padding: 5px; + border: 3px solid red; +} + +.footnote:target { + background-color: #ffa; +} + +.line-block { + display: block; + margin-top: 1em; + margin-bottom: 1em; +} + +.line-block .line-block { + margin-top: 0; + margin-bottom: 0; + margin-left: 1.5em; +} + +.guilabel, .menuselection { + font-family: sans-serif; +} + +.accelerator { + text-decoration: underline; +} + +.classifier { + font-style: oblique; +} + +abbr, acronym { + border-bottom: dotted 1px; + cursor: help; +} + +/* -- code displays --------------------------------------------------------- */ + +pre { + overflow: auto; + overflow-y: hidden; /* fixes display issues on Chrome browsers */ +} + +td.linenos pre { + padding: 5px 0px; + border: 0; + background-color: transparent; + color: #aaa; +} + +table.highlighttable { + margin-left: 0.5em; +} + +table.highlighttable td { + padding: 0 0.5em 0 0.5em; +} + +tt.descname { + background-color: transparent; + font-weight: bold; + font-size: 1.2em; +} + +tt.descclassname { + background-color: transparent; +} + +tt.xref, a tt { + background-color: transparent; + font-weight: bold; +} + +h1 tt, h2 tt, h3 tt, h4 tt, h5 tt, h6 tt { + background-color: transparent; +} + +.viewcode-link { + float: right; +} + +.viewcode-back { + float: right; + font-family: sans-serif; +} + +div.viewcode-block:target { + margin: -1px -10px; + padding: 0 10px; +} + +/* -- math display ---------------------------------------------------------- */ + +img.math { + vertical-align: middle; +} + +div.body div.math p { + text-align: center; +} + +span.eqno { + float: right; +} + +/* -- printout stylesheet --------------------------------------------------- */ + +@media print { + div.document, + div.documentwrapper, + div.bodywrapper { + margin: 0 !important; + width: 100%; + } + + div.sphinxsidebar, + div.related, + div.footer, + #top-link { + display: none; + } +} diff --git a/doc/sphinx/theme/epicsdoc/static/contents.png b/doc/sphinx/theme/epicsdoc/static/contents.png new file mode 100644 index 0000000..7fb8215 Binary files /dev/null and b/doc/sphinx/theme/epicsdoc/static/contents.png differ diff --git a/doc/sphinx/theme/epicsdoc/static/default.css_t b/doc/sphinx/theme/epicsdoc/static/default.css_t new file mode 100644 index 0000000..85c9436 --- /dev/null +++ b/doc/sphinx/theme/epicsdoc/static/default.css_t @@ -0,0 +1,310 @@ +/* + * default.css_t + * ~~~~~~~~~~~~~ + * + * Sphinx stylesheet -- default theme. + * + * :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +@import url("basic.css"); + +/* -- page layout ----------------------------------------------------------- */ + +body { + font-family: {{ theme_bodyfont }}; + font-size: 100%; + background-color: {{ theme_footerbgcolor }}; + color: #000; + margin: 0; + padding: 0; +} + +div.document { + background-color: {{ theme_sidebarbgcolor }}; +} + +div.documentwrapper { + float: left; + width: 100%; +} + +div.bodywrapper { + margin: 0 0 0 {{ theme_sidebarwidth|toint }}px; +} + +div.body { + background-color: {{ theme_bgcolor }}; + color: {{ theme_textcolor }}; + padding: 0 20px 30px 20px; +} + +{%- if theme_rightsidebar|tobool %} +div.bodywrapper { + margin: 0 {{ theme_sidebarwidth|toint }}px 0 0; +} +{%- endif %} + +div.footer { + color: {{ theme_footertextcolor }}; + width: 100%; + padding: 9px 0 9px 0; + text-align: center; + font-size: 75%; +} + +div.footer a { + color: {{ theme_footertextcolor }}; + text-decoration: underline; +} + +div.related { + background-color: {{ theme_relbarbgcolor }}; + line-height: 30px; + color: {{ theme_relbartextcolor }}; +} + +div.related a { + color: {{ theme_relbarlinkcolor }}; +} + +div.sphinxsidebar { + {%- if theme_stickysidebar|tobool %} + top: 30px; + bottom: 0; + margin: 0; + position: fixed; + overflow: auto; + height: auto; + {%- endif %} + {%- if theme_rightsidebar|tobool %} + float: right; + {%- if theme_stickysidebar|tobool %} + right: 0; + {%- endif %} + {%- endif %} +} + +{%- if theme_stickysidebar|tobool %} +/* this is nice, but it it leads to hidden headings when jumping + to an anchor */ +/* +div.related { + position: fixed; +} + +div.documentwrapper { + margin-top: 30px; +} +*/ +{%- endif %} + +div.sphinxsidebar h3 { + font-family: {{ theme_headfont }}; + color: {{ theme_sidebartextcolor }}; + font-size: 1.4em; + font-weight: normal; + margin: 0; + padding: 0; +} + +div.sphinxsidebar h3 a { + color: {{ theme_sidebartextcolor }}; +} + +div.sphinxsidebar h4 { + font-family: {{ theme_headfont }}; + color: {{ theme_sidebartextcolor }}; + font-size: 1.3em; + font-weight: normal; + margin: 5px 0 0 0; + padding: 0; +} + +div.sphinxsidebar p { + color: {{ theme_sidebartextcolor }}; +} + +div.sphinxsidebar p.topless { + margin: 5px 10px 10px 10px; +} + +div.sphinxsidebar ul { + margin: 10px; + padding: 0; + color: {{ theme_sidebartextcolor }}; +} + +div.sphinxsidebar a { + color: {{ theme_sidebarlinkcolor }}; +} + +div.sphinxsidebar input { + border: 1px solid {{ theme_sidebarlinkcolor }}; + font-family: sans-serif; + font-size: 1em; +} + +{% if theme_collapsiblesidebar|tobool %} +/* for collapsible sidebar */ +div#sidebarbutton { + background-color: {{ theme_sidebarbtncolor }}; +} +{% endif %} + +/* -- hyperlink styles ------------------------------------------------------ */ + +a { + color: {{ theme_linkcolor }}; + text-decoration: none; +} + +a:visited { + color: {{ theme_visitedlinkcolor }}; + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +{% if theme_externalrefs|tobool %} +a.external { + text-decoration: none; + border-bottom: 1px dashed {{ theme_linkcolor }}; +} + +a.external:hover { + text-decoration: none; + border-bottom: none; +} + +a.external:visited { + text-decoration: none; + border-bottom: 1px dashed {{ theme_visitedlinkcolor }}; +} +{% endif %} + +/* -- body styles ----------------------------------------------------------- */ + +div.body h1, +div.body h2, +div.body h3, +div.body h4, +div.body h5, +div.body h6 { + font-family: {{ theme_headfont }}; + background-color: {{ theme_headbgcolor }}; + font-weight: normal; + color: {{ theme_headtextcolor }}; + border-bottom: 1px solid #ccc; + margin: 20px -20px 10px -20px; + padding: 3px 0 3px 10px; +} + +div.body h1 { margin-top: 0; font-size: 200%; } +div.body h2 { font-size: 160%; } +div.body h3 { font-size: 140%; } +div.body h4 { font-size: 120%; } +div.body h5 { font-size: 110%; } +div.body h6 { font-size: 100%; } + +a.headerlink { + color: {{ theme_headlinkcolor }}; + font-size: 0.8em; + padding: 0 4px 0 4px; + text-decoration: none; +} + +a.headerlink:hover { + background-color: {{ theme_headlinkcolor }}; + color: white; +} + +div.body p, div.body dd, div.body li { + text-align: justify; + line-height: 130%; +} + +div.admonition p.admonition-title + p { + display: inline; +} + +div.admonition p { + margin-bottom: 5px; +} + +div.admonition pre { + margin-bottom: 5px; +} + +div.admonition ul, div.admonition ol { + margin-bottom: 5px; +} + +div.note { + background-color: #eee; + border: 1px solid #ccc; +} + +div.seealso { + background-color: #ffc; + border: 1px solid #ff6; +} + +div.topic { + background-color: #eee; +} + +div.warning { + background-color: #ffe4e4; + border: 1px solid #f66; +} + +p.admonition-title { + display: inline; +} + +p.admonition-title:after { + content: ":"; +} + +pre { + padding: 5px; + background-color: {{ theme_codebgcolor }}; + color: {{ theme_codetextcolor }}; + line-height: 120%; + border: 1px solid #ac9; + border-left: none; + border-right: none; +} + +tt { + background-color: #ecf0f3; + padding: 0 1px 0 1px; + font-size: 0.95em; +} + +th { + background-color: #ede; +} + +.warning tt { + background: #efc2c2; +} + +.note tt { + background: #d6d6d6; +} + +.viewcode-back { + font-family: {{ theme_bodyfont }}; +} + +div.viewcode-block:target { + background-color: #f4debf; + border-top: 1px solid #ac9; + border-bottom: 1px solid #ac9; +} diff --git a/doc/sphinx/theme/epicsdoc/static/epicsdoc.css_t b/doc/sphinx/theme/epicsdoc/static/epicsdoc.css_t new file mode 100644 index 0000000..6d1ae2b --- /dev/null +++ b/doc/sphinx/theme/epicsdoc/static/epicsdoc.css_t @@ -0,0 +1,514 @@ +/* + * epicsdoc.css_t + * ~~~~~~~~~~~~~~~ + * + * Sphinx stylesheet -- epicsdoc theme. + * a combination of sphinxdoc and default themes + * + */ + +@import url("basic.css"); + +/* -- page layout ----------------------------------------------------------- */ + +body { + font-family: 'Lucida Grande', 'Lucida Sans Unicode', 'Geneva', + 'Verdana', sans-serif; + font-size: 13px; + letter-spacing: -0.01em; + line-height: 150%; + text-align: center; + background-color: #E5E9F0; + color: black; + padding: 0; + border: 1px solid #aaa; + margin: 0px 15px 0px 15px; + min-width: 700px; +} + +div.document { + background-color: white; + text-align: left; + background-image: url(contents.png); + background-repeat: repeat-x; +} + +div.bodywrapper { + margin: 0 0 0 {{ theme_sidebarwidth|toint + 10 }}px; + border-right: 1px solid #ccc; +} + +div.body { + margin: 0; + padding: 0.5em 20px 20px 20px; +} + +div.related { + font-size: 1em; +} + +div.related ul { + background-image: url(navigation.png); + height: 2em; + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; +} + +div.related ul li { + margin: 0; + padding: 0; + height: 2em; + float: left; +} + +div.related ul li.right { + float: right; + margin-right: 5px; +} + +div.related ul li a { + margin: 0; + padding: 0 5px 0 5px; + line-height: 1.25em; + color: #484860; +} + +div.related ul li a:hover { + color: #3CE8E7; +} + +/* -- sidebar --------------------------------------------------------------- */ + +div.sphinxsidebarwrapper { + padding: 0; +} + +div.sphinxsidebar { + margin: 0; + padding: 2px 2px 2px 2px; + font-size: 1em; + text-align: left; + {%- if theme_stickysidebar|tobool %} + position: fixed; + height: auto; + {%- endif %} +} + +{% if theme_collapsiblesidebar|tobool %} /* for collapsible sidebar */ +div#sidebarbutton { + background-color: #E5E9F0; + width: 10 px; +} +div#sidebarbutton.hover { + background-color: #E5E9F0; + width: 10 px; +} + +{% endif %} +/* {{ theme_sidebarbtncolor }}; */ + +div.sphinxsidebar h3, div.sphinxsidebar h4 { + margin: 1em 0 0.5em 0; + font-size: 1em; + padding: 0.1em 0 0.1em 0.5em; + color: #55A; + border: 1px solid #86989B; + background-color: #E5E9F0; +} + +div.sphinxsidebar h3 a { + color: #55A; +} + +div.sphinxsidebar ul { + padding-left: 1.5em; + margin-top: 7px; + padding: 0; + line-height: 130%; +} + +div.sphinxsidebar ul ul { + margin-left: 20px; +} + + +/* -- end sidebar --*/ + +div.footer { + background-color: #E5E9F0; + color: #773; + padding: 3px 8px 3px 0; + clear: both; + font-size: 0.8em; + text-align: right; +} + +div.footer a { + color: #D22; + text-decoration: underline; +} + +/* -- body styles ----------------------------------------------------------- */ + +p { + margin: 0.8em 0 0.5em 0; +} + +a { + color: #CA7900; + text-decoration: none; +} + +a:hover { + color: #2491CF; +} + +div.body a { + text-decoration: underline; +} + +h1 { + margin: 0; + padding: 0.7em 0 0.3em 0; + font-size: 1.5em; + color: #11557C; +} + +h2 { + margin: 1.3em 0 0.2em 0; + font-size: 1.35em; + padding: 0; +} + +h3 { + margin: 1em 0 -0.3em 0; + font-size: 1.2em; +} + +div.body h1 a, div.body h2 a, div.body h3 a, div.body h4 a, div.body h5 a, div.body h6 a { + color: black!important; +} + +h1 a.anchor, h2 a.anchor, h3 a.anchor, h4 a.anchor, h5 a.anchor, h6 a.anchor { + display: none; + margin: 0 0 0 0.3em; + padding: 0 0.2em 0 0.2em; + color: #aaa!important; +} + +h1:hover a.anchor, h2:hover a.anchor, h3:hover a.anchor, h4:hover a.anchor, +h5:hover a.anchor, h6:hover a.anchor { + display: inline; +} + +h1 a.anchor:hover, h2 a.anchor:hover, h3 a.anchor:hover, h4 a.anchor:hover, +h5 a.anchor:hover, h6 a.anchor:hover { + color: #777; + background-color: #eee; +} + +a.headerlink { + color: #c60f0f!important; + font-size: 1em; + margin-left: 6px; + padding: 0 4px 0 4px; + text-decoration: none!important; +} + +a.headerlink:hover { + background-color: #ccc; + color: white!important; +} + +cite, code, tt { + font-family: 'Consolas', 'Deja Vu Sans Mono', + 'Bitstream Vera Sans Mono', monospace; + font-size: 0.95em; + letter-spacing: 0.01em; +} + +tt { + background-color: #f2f2f2; + border-bottom: 1px solid #ddd; + color: #333; +} + +tt.descname, tt.descclassname, tt.xref { + border: 0; +} + +hr { + border: 1px solid #abc; + margin: 2em; +} + +a tt { + border: 0; + color: #CA7900; +} + +a tt:hover { + color: #2491CF; +} + +pre { + font-family: 'Consolas', 'Deja Vu Sans Mono', + 'Bitstream Vera Sans Mono', monospace; + font-size: 0.95em; + letter-spacing: 0.015em; + line-height: 120%; + padding: 0.5em; + border: 1px solid #cdd; + background-color: #fdfdf8; +} + +pre a { + color: inherit; + text-decoration: underline; +} + +td.linenos pre { + padding: 0.5em 0; +} + +div.quotebar { + background-color: #f8f8f8; + max-width: 250px; + float: right; + padding: 2px 7px; + border: 1px solid #ccc; +} + +div.topic { + background-color: #f8f8f8; +} + +table { + border-collapse: collapse; + margin: 0 -0.5em 0 -0.5em; +} + +table td, table th { + padding: 0.25em 0.5em 0.25em 0.5em; +} + +th { + background-color: #fbfbdd; +} + +/* alternating colors in table rows */ +table.docutils tr:nth-child(even) { + background-color: #fcfbfb; +} +table.docutils tr:nth-child(odd) { + background-color: #ffffff; +} + +table.docutils tr { + border-style: solid none solid none; + border-width: 2px 0px 0px 0px; + border-color: #F4F4F4; +} + + +/* for bibliography */ +.bibcite { + margin-top: 0px; + margin-bottom: 0px; + text-indent: 16em; + margin-left: -2em; +} + + +.bib_vol { + font-weight: bold; +} + + +.bib_title { + color: #880000; +} + +div.admonition, div.warning { + font-size: 0.9em; + margin: 1em 0 1em 0; + border: 1px solid #86989B; + background-color: #f7f7f7; + padding: 0; +} + +div.admonition p, div.warning p { + margin: 0.5em 1em 0.5em 1em; + padding: 0; +} + +div.admonition pre, div.warning pre { + margin: 0.4em 1em 0.4em 1em; +} + +div.admonition p.admonition-title, +div.warning p.admonition-title { + margin: 0; + padding: 0.1em 0 0.1em 0.5em; + color: white; + border-bottom: 1px solid #86989B; + font-weight: bold; + background-color: #E5E9F0; +} + +div.warning { + border: 1px solid #940000; +} + +div.warning p.admonition-title { + background-color: #CF0000; + border-bottom-color: #940000; +} + +div.admonition ul, div.admonition ol, +div.warning ul, div.warning ol { + margin: 0.1em 0.5em 0.5em 3em; + padding: 0; +} + +div.versioninfo { + margin: 1em 0 0 0; + border: 1px solid #ccc; + background-color: #DDEAF0; + padding: 8px; + line-height: 1.3em; + font-size: 0.9em; +} + +.viewcode-back { + font-family: 'Lucida Grande', 'Lucida Sans Unicode', 'Geneva', + 'Verdana', sans-serif; +} + +div.viewcode-block:target { + background-color: #f4debf; + border-top: 1px solid #ac9; + border-bottom: 1px solid #ac9; +} + + + +/* change default tables to booktabs style */ +div.body td.align-left { + text-align: left; +} +div.body td.align-right { + text-align: right; +} +div.body td.align-center { + text-align: center; +} +div.body td.align-top { + vertical-align: top; +} +div.body td.align-bottom { + vertical-align: bottom; +} +div.body td.align-middle { + vertical-align: middle; +} +table.docutils th { + border-bottom: 2px solid #AAA; + border-top: 2px solid #AAA; +} +table.docutils td { + border: 0; + padding: 4px 8px 4px 5px; + line-height: 130%; +} +table.docutils { + border-bottom: 2px solid #AAA; + margin: 0; + padding: 0 -0.5em 0 -0.5em; +} + +/* make the title of references bold */ +div#bibliography + table span.title { + font-weight: bold; +} + +/* fix word-wrapping in long http links in references */ +div#bibliography + table a.external { + word-wrap: break-word; + word-break: break-all; +} + +div#bibliography + reference { + white-space: pre; +} + +/* make popup svg images take up the whole window */ +div#colorbox img { + width: 100%; + height: 100%; +} + +/* link color change */ +a, div.related ul li a, div.body a { + color: #08c; + text-decoration: none; +} +a:hover, div.related ul li a:hover, div.body a:hover { + color: #aa71aa; +} +h1, h2 { + color: #333; +} + +/* no background for top bar since it's orange */ +div.related ul { + background-image: none; + background-color: #fff; +} + +/* give figures a nice rounded box style */ +div.figure { + margin: 12px 0px; + padding: 3px 4px; + padding-top: 4px; + border: 1px solid #EE9; + -webkit-border-radius: 4x; + -moz-border-radius: 4px; + border-radius: 6px; +} + + + +/* change figure captions to be aligned nicely */ +div.body p.caption { + text-align: justify; +} +div.subfigure > p.caption { + text-align: center; +} + +/* change figure captions to get highlighted when targeted with a link */ +div.figure:target > p.caption, +div.subfigure:target > p.caption, +div.figure:target > center > p.caption, +div.math:target span.eqno { + background-color: #FFA; +} +div.figure:target, +div.subfigure:target { + border: 1px dashed #333; +} +div.figure.compound { + padding: 0; + padding-top: 5px; +} +div.figure.compound > p.caption { + margin-left: 4px; + margin-right: 4px; +} +div.subfigure { + display: inline-block; + vertical-align: top; + padding-left: 4px; + padding-right: 4px; +} diff --git a/doc/sphinx/theme/epicsdoc/static/navigation.png b/doc/sphinx/theme/epicsdoc/static/navigation.png new file mode 100644 index 0000000..1081dc1 Binary files /dev/null and b/doc/sphinx/theme/epicsdoc/static/navigation.png differ diff --git a/doc/sphinx/theme/epicsdoc/static/sidebar.js b/doc/sphinx/theme/epicsdoc/static/sidebar.js new file mode 100644 index 0000000..1c44012 --- /dev/null +++ b/doc/sphinx/theme/epicsdoc/static/sidebar.js @@ -0,0 +1,151 @@ +/* + * sidebar.js + * ~~~~~~~~~~ + * + * This script makes the Sphinx sidebar collapsible. + * + * .sphinxsidebar contains .sphinxsidebarwrapper. This script adds + * in .sphixsidebar, after .sphinxsidebarwrapper, the #sidebarbutton + * used to collapse and expand the sidebar. + * + * When the sidebar is collapsed the .sphinxsidebarwrapper is hidden + * and the width of the sidebar and the margin-left of the document + * are decreased. When the sidebar is expanded the opposite happens. + * This script saves a per-browser/per-session cookie used to + * remember the position of the sidebar among the pages. + * Once the browser is closed the cookie is deleted and the position + * reset to the default (expanded). + * + * :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +$(function() { + // global elements used by the functions. + // the 'sidebarbutton' element is defined as global after its + // creation, in the add_sidebar_button function + var bodywrapper = $('.bodywrapper'); + var sidebar = $('.sphinxsidebar'); + var sidebarwrapper = $('.sphinxsidebarwrapper'); + + // for some reason, the document has no sidebar; do not run into errors + if (!sidebar.length) return; + + // original margin-left of the bodywrapper and width of the sidebar + // with the sidebar expanded + var bw_margin_expanded = bodywrapper.css('margin-left'); + var ssb_width_expanded = sidebar.width(); + + // margin-left of the bodywrapper and width of the sidebar + // with the sidebar collapsed + var bw_margin_collapsed = '.8em'; + var ssb_width_collapsed = '.8em'; + + // colors used by the current theme + var dark_color = $('.related').css('background-color'); + var light_color = $('.document').css('background-color'); + + function sidebar_is_collapsed() { + return sidebarwrapper.is(':not(:visible)'); + } + + function toggle_sidebar() { + if (sidebar_is_collapsed()) + expand_sidebar(); + else + collapse_sidebar(); + } + + function collapse_sidebar() { + sidebarwrapper.hide(); + sidebar.css('width', ssb_width_collapsed); + bodywrapper.css('margin-left', bw_margin_collapsed); + sidebarbutton.css({ + 'margin-left': '0', + 'height': bodywrapper.height() + }); + sidebarbutton.find('span').text('»'); + sidebarbutton.attr('title', _('Expand sidebar')); + document.cookie = 'sidebar=collapsed'; + } + + function expand_sidebar() { + bodywrapper.css('margin-left', bw_margin_expanded); + sidebar.css('width', ssb_width_expanded); + sidebarwrapper.show(); + sidebarbutton.css({ + 'margin-left': ssb_width_expanded-8, + 'height': bodywrapper.height() + }); + sidebarbutton.find('span').text('«'); + sidebarbutton.attr('title', _('Collapse sidebar')); + document.cookie = 'sidebar=expanded'; + } + + function add_sidebar_button() { + sidebarwrapper.css({ + 'float': 'left', + 'margin-right': '0', + 'width': ssb_width_expanded - 20 + }); + // create the button + sidebar.append( + '
    «
    ' + ); + var sidebarbutton = $('#sidebarbutton'); + light_color = sidebarbutton.css('background-color'); + // find the height of the viewport to center the '<<' in the page + var viewport_height; + if (window.innerHeight) + viewport_height = window.innerHeight; + else + viewport_height = $(window).height(); + sidebarbutton.find('span').css({ + 'display': 'block', + 'margin-top': (viewport_height - sidebar.position().top - 20) / 2 + }); + + sidebarbutton.click(toggle_sidebar); + sidebarbutton.attr('title', _('Collapse sidebar')); + sidebarbutton.css({ + 'color': '#FFFFFF', + 'border-left': '1px solid ' + dark_color, + 'font-size': '1.2em', + 'cursor': 'pointer', + 'height': bodywrapper.height(), + 'padding-top': '1px', + 'margin-left': ssb_width_expanded - 10 + }); + + sidebarbutton.hover( + function () { + $(this).css('background-color', '#AFC1C4'); + }, + function () { + $(this).css('background-color', '#AFC1C4'); + } + ); + } + + function set_position_from_cookie() { + if (!document.cookie) + return; + var items = document.cookie.split(';'); + for(var k=0; kwidget classes + below. + +.. method:: SetPV(pv=None) + + set the PV corresponding to the widget. + +.. method:: Update(value=None) + + set the widgets value from the PV's value. If value=``None``, the current + value for the PV is used. + +.. method:: GetValue(as_string=True) + + return the PVs value. + +.. method:: OnEpicsConnect() + + PV connection event handler. + +.. method:: OnPVChange(value) + + PV monitor (subscription) event handler. Must be overwritten for each + widget type. + +.. method:: GetEnumStrings() + + return enumeration strings for the PV + + +PVCtrlMixin +~~~~~~~~~~~~ + +.. class:: PVCtrlMixin(parent, pv=None, font=None, fg=None, bg=None, **kw) + + This is a mixin class for wx Controls with epics PVs: This subclasses + PVCtrlMixin and adds colour translations + PV, and manages callback events for the PV. + + :param parent: wx parent widget + :param pv: epics.PV + :param font: wx.Font for display + :param fg: foreground colour + :param bg: background colour + + + A class that inherits from this class **must** provide a method called + `_SetValue`, which will set the contents of the corresponding widget + when the PV's value changes. + + In general, the widgets will automatically update when the PV + changes. Where appropriate, setting the value with the widget will set + the PV value. + + +PVText +~~~~~~~~~ + + +.. class:: PVText(parent, pv=None, font=None, fg=None, bg=None, minor_alarm="DARKRED", major_alarm="RED", invalid_alarm="ORANGERED", auto_units=False, units="", **kw) + + derived from wx.StaticText and PVCtrlMixin, this is a StaticText widget + whose value is set to the string representation of the value for the + corresponding PV. + + By default, the text colour will be overridden when the PV enters an + alarm state. These colours can be modified (or disabled by being set + to ``None``) as part of the constructor. + + "units" specifies a unit suffix (like ' A' or ' mm') to put after the text + value whenever it is displayed. + + Alternatively, "auto_units" means the control will automatically display + the "EGU" units value from the PV, whenever it updates. If this value is + set, "units" is ignored. A space is inserted between the value and the + unit. + + +PVTextCtrl +~~~~~~~~~~~ + +.. class:: PVTextCtrl(parent, pv=None, font=None, fg=None, bg=None, dirty_timeout=2500, **kw) + + derived from wx.TextCtrl and PVCtrlMixin, this is a TextCtrl widget + whose value is set to the string representation of the value for the + corresponding PV. + + Setting the value (hitting Return or Enter) or changing focus away + from the widget will set the PV value immediately. Otherwise, the + widget will wait for 'dirty_timeout' milliseconds after the last + keypress and then set the PV value to whatever is written in the field. + + +PVFloatCtrl +~~~~~~~~~~~ + +.. class:: PVFloatCtrl(parent, pv=None, font=None, fg=None, bg=None, **kw) + + A special variation of a wx.TextCtrl that allows only floating point + numbers, as associated with a double, float, or integer PV. Trying to + type in a non-numerical value will be ignored. Furthermore, if a PV's + limits can be determined, they will be used to limit the allowed range + of input values. For a value that is within limits, the value will be + `put` to the PV on return. Out-of-limit values will be highlighted in + a different color. + + +PVBitmap +~~~~~~~~~~~ + +.. class:: PVBitmap(parent, pv=None, bitmaps={}, defaultBitmap=None) + + A Static Bitmap where the image is based on PV value. + + If the bitmaps dictionary is set, it should be set as PV.Value(Bitmap) + where particular bitmaps will be shown if the PV takes those certain values. + + If you need to do any more complex or dynamic drawing, you may want to look at the OGL PV controls. + + +PVCheckBox +~~~~~~~~~~~ + +.. class:: PVCheckBox(self, parent, pv=None, on_value=1, off_value=0, **kw) + + Checkbox based on a binary PV value, both reads/writes the PV on + changes. on_value and off_value are the specific values that are + mapped to the checkbox. + + There are multiple options for translating PV values to checkbox + settings (from least to most complex): + + * Use a PV with values 0 and 1 + * Use a PV with values that convert via Python's own bool(x) + * Set on_value and off_value in the constructor + * Use SetTranslations() to set a dictionary for converting various + PV values to booleans. + + +PVFloatSpin +~~~~~~~~~~~ + +.. class:: PVFloatSpin(parent, pv=None, deadTime=500, min_val=None, max_val=None, increment=1.0, digits=-1, **kw) + + A FloatSpin is a floating point spin control with buttons to increase + and decrease the value by a particular increment. Arrow keys and page + up/down can also be used (the latter changes the value by 10x the + increment.) + + PVFloatSpin is a special derivation that assigns a PV to the FloatSpin + control. deadTime is the delay (in milliseconds) between when the user + finishes typing a value and when the PV is set to it (to prevent + half-typed numeric values being set.) + + +PVButton +~~~~~~~~~~~ + +.. class:: PVButton(parent, pv=None, pushValue=1, disablePV=None, + disableValue=1, **kw) + + A wx.Button linked to a PV. When the button is pressed, 'pushValue' is + written to the PV (useful for momentary PVs with HIGH= set.) Setting + disablePV and disableValue will automatically cause the button to + disable when that PV has a certain value. + + +PVRadioButton +~~~~~~~~~~~~~ + +.. class:: PVRadioButton(parent, pv=None, pvValue=None, **kw) + + A PVRadioButton is a radio button associated with a particular PV and + one particular value. + + Suggested for use in a group where all radio buttons are + PVRadioButtons, and they all have a discrete value set. + + +PVComboBox +~~~~~~~~~~~ + +.. class:: PVComboBox(parent, pv=None, **kw) + + A ComboBox linked to a PV. Both reads/writes the combo value on + changes. + + +PVEnumComboBox +~~~~~~~~~~~~~~~~ + +.. class:: PVEnumComboBox(parent, pv=None, **kw) + + A ComboBox linked to an "enum" type PV (such as bi,bo,mbbi,mbbo.) The ComboBox + is automatically populated with a non-editable list of the PV enum values, allowing + the user to select them from the dropdown. + + Both reads/writes the combo value on changes. + + +PVEnumButtons +~~~~~~~~~~~~~~~~~~ + +.. class:: PVEnumButtons(parent, pv=None, font=None, fg=None, bg=None, **kw) + + This will create a wx.Panel of buttons (a button bar), 1 for each + enumeration state of an enum PV. The set of buttons will correspond to + the current state of the PV + + +PVEnumChoice +~~~~~~~~~~~~~~~~~~ + +.. class:: PVEnumChoice(parent, pv=None, font=None, fg=None, bg=None, **kw) + + This will create a dropdown list (a wx.Choice) with a list of + enumeration states for an enum PV. + + +PVAlarm +~~~~~~~~~~ + +.. class:: PVAlarm(parent, pv=None, font=None, fg=None, bg=None, trip_point=None, **kw) + + This will create a pop-up message (wx.MessageDialog) that is shown when + the corresponding PV trips the alarm level. + +PVCollapsiblePane +~~~~~~~~~~~~~~~~~ + +.. class:: PVCollapsiblePane(parent, pv=None, minor_alarm="DARKRED", major_alarm="RED", invalid_alarm="ORANGERED", **kw) + + This is equivalent to wx.CollapsiblePane, except the label shown + on the pane's "expansion button" comes from a PV. + + The additional keyword arguments can be any of the other constructor + arguments supported by wx.CollapsiblePane. + + By default, the foreground colour of the pane button will be overridden + when the PV enters an alarm state. On GTK, this means the colour of the + triangular drop-down button but not the label text. These colours can + be modified (or disabled by being set to ``None``) as part of the + constructor. + + Supports the .SetTranslation() method, whose argument is a dictionary + mapping PV values to display labels. If the PV value is not found in + the dictionary, it will displayed verbatim as the label. + + +Decorators and other Utility Functions +========================================== + + +.. function:: DelayedEpicsCallback + +decorator to wrap an Epics callback in a wx.CallAfter, +so that the wx and epics ca threads do not clash +This also checks for dead wxPython objects (say, from a +closed window), and remove callbacks to them. + +.. function:: EpicsFunction + +decorator to wrap function in a wx.CallAfter() so that +Epics calls can be made in a separate thread, and asynchronously. + +This decorator should be used for all code that mix calls to wx and epics + +.. function:: finalize_epics + +This function will finalize epics, and close all Channel Access +communication, by calling :meth:`epics.ca.finalize_libca`. This may be +useful when closing an application, as in a method bound to `wx.EVT_CLOSE` +event from a top-level application window. Be careful to **not** call this +function when closing a Window if your application is not closing, and if +you are still doing any Channel Access work in the other windows. + + + +wxMotorPanel Widget +======================== + +A dedicated wx Widget for Epics Motors is included in the :mod:`wx` module +that provides an easy-to-use Motor panel that is similar to the normal MEDM +window, but with a few niceties from the more sophisticated wx +toolkit. This widget can be used simply as:: + + import wx + from epics.wx import MotorPanel + .... + mymotor = MotorPanel(parent, 'XXX:m1') + +A sample panel looks like this + +.. image:: wx_motor.png + +Which shows from right to left: the motor description, an information +message (blank most of the time), the readback value, the drive value, +arrows to tweak the motor, and a drop-down combobox for tweak values, a +"Stop" button and a "More" button. The panel has the following features: + + * All controls are "live" and will respond to changes from other source. + * The values for the tweak values in the ComboBox are automatically + generated from the precision and travel range of the motor. + * The entry box for the drive value will *only* accept numeric input, + and will only set the drive value when hitting Enter or Return. + * The drive value will change to Red text on a Yellow background when + the value in the box violates the motors (user) limits. If Enter or + Return when the the displayed value violates the limit, the motor + will not be moved, but the displayed value will be changed to the + closest limit value. + * Pressing the "Stop" button will stop the motor (with the `.SPMG` + field), and set the Info field to "Stopped". The button label will + change to "Go", and the motor will not move until this button is pressed. + +Finally, the "More" button will bring up a more complete form of Motor +parameters that looks like: + +.. image:: wx_motordetail.png + +Many such MotorPanels can be put in a vertical stack, as generated from the +'wx_motor.py' script in the scripts folder of the source distribution as:: + + ~>python wx_motor.py XXX:m1 XXX:m2 XXX:m3 XXX:m4 + +will look like this: + +.. image:: wx_motor_many.png + + +OGL Classes +=========== + +OGL is a graphics drawing library shipped with wxPython. Is it built around +the concept of "shapes" which are added to "canvases" and can be moved, +scrolled, zoomed, animated, etc. + +There is a PVShapeMixin class which allows PV callback functionality to be +added to any OGL Shape class, and there are also PVRectangle and PVCircle +subclasses already created. + +A recommended way to use these OGL classes is to make a static bitmap +background for your display, place it in an OGL Canvas and then add an +overlay of shapes which appear/disappear/resize/change colour based on +the PV values. + +PVShapeMixin +~~~~~~~~~~~~~~~~ + +.. class:: PVShapeMixin(self, pv=None, pvname=None) + + Similar to PVMixin, this mixin should be added to any + ogl.Shape subclass that needs PV callback support. + + The main method is PVChanged(self, raw_value), which should be + overridden in the subclass to provide specific processing based on + the changed value. + + There are also some built-in pieces of functionality. These are + enough to do simple show/hide or change colour shape functionality, + without needing to write specific code. + + SetBrushTranslations(translations) allows setting a dict of PV Value -> + wx.Brush mappings, which can be used to automatically repaint the shape + foreground (fill) when the PV changes. + + SetPenTranslations(translations) similar to brush translations, but + the values are wx.Pen instances that are used to repaint the shape + outline when the PV changes. + + SetShownTranslations(translations) sets a dictionary of PV Value ->bool + values which are used to show/hide the shape depending on the PV value, + as it changes. + + +PVRectangle +~~~~~~~~~~~ + +.. class:: PVRectangle(self, w, h, pv=None, pvname=None) + + A PVCtrlMixin for the Rectangle shape class. + + +PVCircle +~~~~~~~~ + +.. class:: PVCircle(self, diameter, pv=None, pvname=None) + + A PVCtrlMixin for the Circle shape class. diff --git a/doc/wx_motor.png b/doc/wx_motor.png new file mode 100644 index 0000000..5bbe548 Binary files /dev/null and b/doc/wx_motor.png differ diff --git a/doc/wx_motor_many.png b/doc/wx_motor_many.png new file mode 100644 index 0000000..91cd231 Binary files /dev/null and b/doc/wx_motor_many.png differ diff --git a/doc/wx_motordetail.png b/doc/wx_motordetail.png new file mode 100644 index 0000000..8580475 Binary files /dev/null and b/doc/wx_motordetail.png differ diff --git a/epics/__init__.py b/epics/__init__.py new file mode 100644 index 0000000..3ee46fe --- /dev/null +++ b/epics/__init__.py @@ -0,0 +1,213 @@ +from ._version import get_versions +__version__ = get_versions()['version'] +del get_versions + +__doc__ = """ + epics channel access python module + + version: %s + Principal Authors: + Matthew Newville CARS, University of Chicago + Angus Gratton , Australian National University + +== License: + + Except where explicitly noted, this file and all files in this + distribution are licensed under the Epics Open License See license.txt in + the top-level directory of this distribution. + +== Overview: + Python Interface to the Epics Channel Access + protocol of the Epics control system. + +""" % (__version__) + + +import time +import sys +import threading +from . import ca +from . import dbr +from . import pv +from . import alarm +from . import device +from . import motor +from . import multiproc + +PV = pv.PV +Alarm = alarm.Alarm +Motor = motor.Motor +Device = device.Device +poll = ca.poll + +get_pv = pv.get_pv + +CAProcess = multiproc.CAProcess +CAPool = multiproc.CAPool + +# some constants +NO_ALARM = 0 +MINOR_ALARM = 1 +MAJOR_ALARM = 2 +INVALID_ALARM = 3 + +_PVmonitors_ = {} + +def caput(pvname, value, wait=False, timeout=60): + """caput(pvname, value, wait=False, timeout=60) + simple put to a pv's value. + >>> caput('xx.VAL',3.0) + + to wait for pv to complete processing, use 'wait=True': + >>> caput('xx.VAL',3.0,wait=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,count=None,as_numpy=True, + use_monitor=False,timeout=5.0) + simple get of a pv's value.. + >>> x = caget('xx.VAL') + + to get the character string representation (formatted double, + enum string, etc): + >>> x = caget('xx.VAL', as_string=True) + + to get a truncated amount of data from an array, you can specify + the count with + >>> x = caget('MyArray.VAL', count=1000) + """ + start_time = time.time() + thispv = get_pv(pvname, timeout=timeout, connect=True) + if thispv.connected: + if as_string: + thispv.get_ctrlvars() + timeout -= (time.time() - start_time) + val = thispv.get(count=count, timeout=timeout, + use_monitor=use_monitor, + as_string=as_string, + as_numpy=as_numpy) + poll() + return val + +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') + + will return a status report for the pv. + + If print_out=False, the status report will be printed, + and not returned. + """ + start_time = time.time() + thispv = get_pv(pvname, timeout=timeout, connect=True) + if thispv.connected: + 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: + return thispv.info + +def camonitor_clear(pvname): + """clear a monitor on a PV""" + if pvname in _PVmonitors_: + _PVmonitors_[pvname].remove_callback(index=-999) + _PVmonitors_.pop(pvname) + +def camonitor(pvname, writer=None, callback=None): + """ camonitor(pvname, writer=None, callback=None) + + sets a monitor on a PV. + >>>camonitor('xx.VAL') + + This will write a message with the latest value for that PV each + time the value changes and when ca.poll() is called. + + To write the result to a file, provide the writer option a write method + to an open file or some other method that accepts a string. + + To completely control where the output goes, provide a callback method + and you can do whatever you'd like with them. + + Your callback will be sent keyword arguments for pvname, value, and + char_value Important: use **kwd!! + """ + + if writer is None: + writer = ca.write + if callback is None: + def callback(pvname=None, value=None, char_value=None, **kwds): + "generic monitor callback" + if char_value is None: + char_value = repr(value) + writer("%.32s %s %s" % (pvname, pv.fmt_time(), char_value)) + + thispv = get_pv(pvname, connect=True) + if thispv.connected: + thispv.get() + thispv.add_callback(callback, index=-999, with_ctrlvars=True) + _PVmonitors_[pvname] = thispv + +def caget_many(pvlist, as_string=False, count=None, as_numpy=True, timeout=5.0): + """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, count=count, as_string=as_string, as_numpy=as_numpy, wait=False) + for chid in chids: out.append(ca.get_complete(chid, + count=count, + as_string=as_string, + as_numpy=as_numpy, + timeout=timeout)) + 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 new file mode 100644 index 0000000..5f426cd --- /dev/null +++ b/epics/_version.py @@ -0,0 +1,21 @@ + +# This file was generated by 'versioneer.py' (0.18) from +# revision-control system data, or from the parent directory name of an +# unpacked source archive. Distribution tarballs contain a pre-generated copy +# of this file. + +import json + +version_json = ''' +{ + "date": "2020-01-09T15:34:58-0600", + "dirty": false, + "error": null, + "full-revisionid": "3af493d29852a0ac7dd43e7e39f1ccda353a3721", + "version": "3.4.1" +} +''' # END VERSION_JSON + + +def get_versions(): + return json.loads(version_json) diff --git a/epics/alarm.py b/epics/alarm.py new file mode 100644 index 0000000..31e698f --- /dev/null +++ b/epics/alarm.py @@ -0,0 +1,154 @@ + #!/usr/bin/env python +# M Newville +# The University of Chicago, 2010 +# Epics Open License +""" +alarm module -- Alarm class +""" +import sys +import time +import operator +from . import pv +from .utils import is_string + +class Alarm(object): + """ alarm class for a PV: + run a user-supplied callback when a PV's value goes out of range + + quick synopsis: + The supplied callback will be run when a comparison of the PV's + value and a trip point is True. An optional alert delay can be + set to limit how frequently the callback is run + + arguments: + pvname name of PV for which to set alarm + trip_point value of trip point + comparison a string for the comparison operation: one of + 'eq', 'ne', 'le', 'lt', 'ge', 'gt' + '==', '!=', '<=', '<' , '>=', '>' + callback function to run when comparison(value,trip_point) is True + 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): + >>> print 'Alarm!! ', pvname, value + >>> alarm(pvname = 'XX.VAL', + >>> comparison='gt', + >>> callback = alarmHandler, + >>> trip_point=2.0, + >>> alert_delay=600) + >>> while True: + >>> 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 + + time period to NOT send messages after a message has been + 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 + self.user_callback(pvname=pvname, value=value, + char_value=char_value, + trip_point=self.trip_point, + comparison=self.cmp.__name__, **kw) + + """ + ops = {'eq': operator.__eq__, + '==': operator.__eq__, + 'ne': operator.__ne__, + '!=': operator.__ne__, + 'le': operator.__le__, + '<=': operator.__le__, + 'lt': operator.__lt__, + '<' : operator.__lt__, + 'ge': operator.__ge__, + '>=': operator.__ge__, + 'gt': operator.__gt__, + '>' : operator.__gt__ } + + def __init__(self, pvname, comparison=None, trip_point=None, + callback=None, alert_delay=10): + + if isinstance(pvname, pv.PV): + self.pv = pvname + 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 + self.alert_delay = alert_delay + self.user_callback = callback + + self.cmp = None + self.comp_name = 'Not Defined' + if callable(comparison): + self.comp_name = comparison.__name__ + 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" + 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. + """ + if (pvname is None or value is None or + self.cmp is None or self.trip_point is None): return + + val = value + if char_value is None: + char_value = value + old_alarm_state = self.alarm_state + self.alarm_state = self.cmp(val, self.trip_point) + + 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 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/__init__.py b/epics/autosave/__init__.py new file mode 100644 index 0000000..7ebbcdb --- /dev/null +++ b/epics/autosave/__init__.py @@ -0,0 +1,16 @@ +""" +This module provides simple save/restore functionality for PVs, similar to +the autosave module in synApps for IOCs but (obviously) via Channel Access. + +Request & Save file formats are designed to be compatible with synApps autosave. + +Use of this module requires the pyparsing parser framework. +The Debian/Ubuntu package is "python-pyparsing" +The web site is http://pyparsing.wikispaces.com/ + +""" +from . import save_restore + +AutoSaver = save_restore.AutoSaver +restore_pvs = save_restore.restore_pvs +save_pvs = save_restore.save_pvs diff --git a/epics/autosave/save_restore.py b/epics/autosave/save_restore.py new file mode 100755 index 0000000..cd0e5aa --- /dev/null +++ b/epics/autosave/save_restore.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python +""" +A python module that uses pyepics to save/restore sets of pvs from files. + +Copyright 2011 Angus Gratton +Australian National University +EPICS Open License + +The module is intended to be compatible with the 'autosave' module format used in synApps. + +Files - + +xxx.req - A request file with a list of pvs to save. Format is the same as autosave request format, + including being able to have "file yyy.req VAR=A,OTHER=B" style macro expansions. + +xxx.sav - A saved file with the current PV values, to save/restore. Standalone file, this is a + compatible format to the .sav files which are used by autosave. + +This module requires/uses pyparsing parser framework. Debian/Ubuntu package is "python-pyparsing" +Web site is http://pyparsing.wikispaces.com/ + +""" + +from pyparsing import (Literal, Optional, Word, Combine, Regex, Group, + ZeroOrMore, OneOrMore, LineEnd, LineStart, StringEnd, + alphanums, alphas, nums, printables) + +import sys +import os +import datetime +import json +from epics.pv import get_pv + +def restore_pvs(filepath, debug=False): + """ + Restore pvs from a save file via Channel Access + + debug - Set to True if you want a line printed for each value set + + Returns True if all pvs were restored successfully. + """ + pv_vals = [] + failures = [] + # preload PV names and values, hoping PV connections happen in background + with open(filepath, 'r') as fh: + for line in fh.readlines(): + if len(line) < 2 or line.startswith(' 1 and 'char' in thispv.type: + value = thispv.get(as_string=True) + elif thispv.count > 1 and 'char' not in thispv.type: + value = '@array@ %s' % json.dumps(thispv.get().tolist()) + buff.append("%s %s" % (pvname, value)) + if verbose: + print( "PV %s = %s" % (pvname, value)) + elif verbose: + print("PV %s not connected" % (pvname)) + + + buff.append("\n") + with open(save_file, 'w') as fh: + fh.write("\n".join(buff)) + print("wrote %s"% save_file) + +def _parse_request_file(request_file, macro_values={}): + """ + Internal function to parse a request file. + + Parse happens in two stages, first build an AST then walk it and do + file expansions (which recurse through here.) + + Returns a list of PV names. + + """ + ast = [ x for x in req_file.parseFile(request_file).asList() if len(x) > 0 ] + + result = [] + for n in ast: + if len(n) == 1: # simple PV name + pvname = n[0] + for m,v in macro_values.items(): # please forgive me this awful macro expansion method + pvname = pvname.replace("$(%s)" % m, v) + result.append(pvname) + elif n[0] == 'file': # include file + subfile = n[1] + subfile = os.path.normpath(os.path.join(os.path.dirname(request_file), subfile)) + sub_macro_vals = macro_values.copy() + sub_macro_vals.update(dict(n[2:])) + result += _parse_request_file(subfile, sub_macro_vals) + else: + raise Exception("Unexpected entry parsed from request file: %s" % n) + return result + +# request & save file grammar (combined because lots of it is pretty similar) +point = Literal('.') +minus = Literal('-') +ignored_quote = Literal('"').suppress() +ignored_comma = Literal(',').suppress() + +file_name = Word(alphanums+":._-+/\\") + +number = Word(nums) +integer = Combine( Optional(minus) + number ) +float_number = Combine( integer + + Optional( point + Optional(number) ) + ).setParseAction(lambda t:float(t[0])) + +# 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_assignment = pv_name + pv_value + +comment = Literal("#") + Regex(r".*") + +macro = Group( Word(alphas) + Literal("=").suppress() + pv_name ) +macros = Optional(macro + ZeroOrMore(Word(";,").suppress() + macro) ) + +#file_include = Literal("file") + pv_name + macros +file_include = Literal("file") + \ + (file_name | ignored_quote + file_name + ignored_quote) \ + + Optional(ignored_comma) + macros + +def line(contents): + return LineStart() + ZeroOrMore(Group(contents)) + LineEnd().suppress() + +req_line = line( file_include | comment.suppress() | pv_name ) +req_file = OneOrMore(req_line) + StringEnd().suppress() + +sav_line = line( comment.suppress() | Literal("").suppress() | pv_assignment) +sav_file = OneOrMore(sav_line) + StringEnd().suppress() diff --git a/epics/ca.py b/epics/ca.py new file mode 100755 index 0000000..5c464ed --- /dev/null +++ b/epics/ca.py @@ -0,0 +1,2016 @@ +#!usr/bin/python +# +# low level support for Epics Channel Access +# +# M Newville +# The University of Chicago, 2010 +# Epics Open License +""" +EPICS Channel Access Interface + +See doc/ for user documentation. + +documentation here is developer documentation. +""" +import ctypes +import ctypes.util + +import atexit +import collections +import functools +import os +import sys +import threading +import time +import warnings + +from math import log10 +from pkg_resources import resource_filename + +from .utils import (STR2BYTES, BYTES2STR, NULLCHAR_2, + strjoin, memcopy, is_string, is_string_or_bytes, + ascii_string, clib_search_path) + +# ignore warning about item size... for now?? +warnings.filterwarnings('ignore', + 'Item size computed from the PEP 3118*', + RuntimeWarning) + +HAS_NUMPY = False +try: + import numpy + HAS_NUMPY = True +except ImportError: + pass + +from . import dbr +from .dbr import native_type + +## print to stdout +def write(msg, newline=True, flush=True): + """write message to stdout""" + sys.stdout.write(msg) + if newline: + sys.stdout.write("\n") + if flush: + sys.stdout.flush() + +## holder for shared library +libca = None +initial_context = None +error_message = '' + +## PREEMPTIVE_CALLBACK determines the CA context +PREEMPTIVE_CALLBACK = True + +AUTO_CLEANUP = True + +## +# maximum element count for auto-monitoring of PVs in epics.pv +# and for automatic conversion of numerical array data to numpy arrays +AUTOMONITOR_MAXLENGTH = 65536 # 16384 + +## default timeout for connection +# This should be kept fairly short -- +# as connection will be tried repeatedly +DEFAULT_CONNECTION_TIMEOUT = 2.0 + +## Cache of existing channel IDs: +# 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) + +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""" + def __init__(self, *args): + Exception.__init__(self, *args) + type_, value, traceback = sys.exc_info() + 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""" + def __init__(self, fcn, msg): + Exception.__init__(self) + self.fcn = fcn + self.msg = msg + 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 + """ + # Test 1: if PYEPICS_LIBCA env var is set, use it. + dllpath = os.environ.get('PYEPICS_LIBCA', None) + + # find libCom.so *next to* libca.so if PYEPICS_LIBCA was set + 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 + + # Test 2: look in installed python location for dll + dllpath = resource_filename('epics.clibs', clib_search_path(inp_lib_name)) + + if (os.path.exists(dllpath) and os.path.isfile(dllpath)): + return dllpath + + # Test 3: look through Python path and PATH env var for dll + path_sep = ':' + dylib = 'lib' + # For windows, we assume the DLLs are installed with the library + if os.name == 'nt': + path_sep = ';' + dylib = 'DLLs' + + basepath = os.path.split(os.path.abspath(__file__))[0] + parent = os.path.split(basepath)[0] + _path = [basepath, parent, + os.path.join(parent, dylib), + os.path.split(os.path.dirname(os.__file__))[0], + os.path.join(sys.prefix, dylib)] + + def envpath2list(envname, path_sep): + plist = [''] + try: + plist = os.environ.get(envname, '').split(path_sep) + except AttributeError: + pass + return plist + + env_path = envpath2list('PATH', path_sep) + ldname = 'LD_LIBRARY_PATH' + if sys.platform == 'darwin': + ldname = 'DYLD_LIBRARY_PATH' + env_ldpath = envpath2list(ldname, path_sep) + + search_path = [] + for adir in (_path + env_path + env_ldpath): + if adir not in search_path and os.path.isdir(adir): + search_path.append(adir) + + os.environ['PATH'] = path_sep.join(search_path) + # with PATH set above, the ctypes utility, find_library *should* + # find the dll.... + dllpath = ctypes.util.find_library(inp_lib_name) + if dllpath is not None: + return dllpath + + raise ChannelAccessException('cannot find Epics CA DLL') + + +def find_libca(): + return _find_lib('ca') + +def find_libCom(): + return _find_lib('Com') + +def initialize_libca(): + """Initialize the Channel Access library. + + This loads the shared object library (DLL) to establish Channel Access + Connection. The value of :data:`PREEMPTIVE_CALLBACK` sets the pre-emptive + callback model. + + This **must** be called prior to any actual use of the CA library, but + will be called automatically by the the :func:`withCA` decorator, so + you should not need to call this directly from most real programs. + + Returns + ------- + libca : object + ca library object, used for all subsequent ca calls + + See Also + -------- + withCA : decorator to ensure CA is initialized + + Notes + ----- + This function must be called prior to any real CA calls. + + """ + if 'EPICS_CA_MAX_ARRAY_BYTES' not in os.environ: + os.environ['EPICS_CA_MAX_ARRAY_BYTES'] = "%i" % 2**24 + + global libca, initial_context + + if os.name == 'nt': + load_dll = ctypes.windll.LoadLibrary + else: + load_dll = ctypes.cdll.LoadLibrary + try: + # force loading the chosen version of libCom + if os.name == 'nt': + load_dll(find_libCom()) + libca = load_dll(find_libca()) + except Exception as exc: + raise ChannelAccessException('loading Epics CA DLL failed: ' + str(exc)) + + ca_context = {False:0, True:1}[PREEMPTIVE_CALLBACK] + ret = libca.ca_context_create(ca_context) + if ret != dbr.ECA_NORMAL: + raise ChannelAccessException('cannot create Epics CA Context') + + # set argtypes and non-default return types + # for several libca functions here + libca.ca_pend_event.argtypes = [ctypes.c_double] + libca.ca_pend_io.argtypes = [ctypes.c_double] + libca.ca_client_status.argtypes = [ctypes.c_void_p, ctypes.c_long] + libca.ca_sg_block.argtypes = [ctypes.c_ulong, ctypes.c_double] + + libca.ca_current_context.restype = ctypes.c_void_p + libca.ca_version.restype = ctypes.c_char_p + libca.ca_host_name.restype = ctypes.c_char_p + libca.ca_name.restype = ctypes.c_char_p + # libca.ca_name.argstypes = [dbr.chid_t] + # libca.ca_state.argstypes = [dbr.chid_t] + libca.ca_message.restype = ctypes.c_char_p + libca.ca_attach_context.argtypes = [ctypes.c_void_p] + + # save value offests used for unpacking + # TIME and CTRL data as an array in dbr module + + # in_dll is not available for arrays in IronPython, so use a reference to the first element + if dbr.IRON_PYTHON: + value_offset0 = ctypes.c_short.in_dll(libca,'dbr_value_offset') + dbr.value_offset = ctypes.cast(ctypes.addressof(value_offset0), + (39*ctypes.c_short)) + else: + dbr.value_offset = (39*ctypes.c_short).in_dll(libca,'dbr_value_offset') + + initial_context = current_context() + if AUTO_CLEANUP: + atexit.register(finalize_libca) + return libca + +def finalize_libca(maxtime=10.0): + """shutdown channel access: + + run :func:`clear_channel` for all chids in :data:`_cache`, + then calls :func:`flush_io` and :func:`poll` a few times. + + Parameters + ---------- + maxtime : float + maximimum time (in seconds) to wait for :func:`flush_io` and :func:`poll` to complete. + + """ + global libca + if libca is None: + return + try: + start_time = time.time() + flush_io() + poll() + 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): + flush_io() + poll() + flush_count += 1 + context_destroy() + libca = None + except: + pass + time.sleep(0.01) + + +def get_cache(pvname): + "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. + """ + 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 + if len(vname) < 15: + vname = (vname + ' '*15)[:15] + out.append(" %s %s/%s %s" % (vname, repr(chid), + repr(context), + isConnected(chid))) + out = strjoin('\n', out) + if print_out: + write(out) + else: + return out + +def clear_cache(): + """ + Clears global caches of Epics CA connections, and fully + detaches from the CA context. This is important when doing + multiprocessing (and is done internally by CAProcess), + but can be useful to fully reset a Channel Access session. + """ + + # Clear global state variables + _cache.clear() + _chid_cache.clear() + + # Clear the cache of PVs used by epics.caget()-like functions + from . import pv + pv._PVcache_ = {} + + # The old context is copied directly from the old process + # in systems with proper fork() implementations + detach_context() + create_context() + + +## decorator functions for ca functionality: +# decorator name ensures before running decorated function: +# -------------- ----------------------------------------------- +# withCA libca is initialized +# withCHID 1st arg is a chid (dbr.chid_t) +# withConnectedCHID 1st arg is a connected chid. +# withInitialContext Force the use of the initially defined context +# +# These tests are not rigorous CA tests (and ctypes.long is +# accepted as a chid, connect_channel() is tried, but may fail) +## +def withCA(fcn): + """decorator to ensure that libca and a context are created + prior to function calls to the channel access library. This is + intended for functions that need CA started to work, such as + :func:`create_channel`. + + 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() + return fcn(*args, **kwds) + return wrapper + +def withCHID(fcn): + """decorator to ensure that first argument to a function is a Channel + ID, ``chid``. The test performed is very weak, as any ctypes long or + python int will pass, but it is useful enough to catch most accidental + errors before they would cause a crash of the CA library. + """ + # 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" + if len(args)>0: + chid = args[0] + args = list(args) + if isinstance(chid, int): + args[0] = chid = dbr.chid_t(args[0]) + if not isinstance(chid, dbr.chid_t): + 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) + 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" + 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) + 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)) + + return fcn(*args, **kwds) + 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" + 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) + return wrapper + +def PySEVCHK(func_name, status, expected=dbr.ECA_NORMAL): + """This checks the return *status* returned from a `libca.ca_***` and + raises a :exc:`ChannelAccessException` if the value does not match the + *expected* value (which is nornmally ``dbr.ECA_NORMAL``. + + The message from the exception will include the *func_name* (name of + the Python function) and the CA message from :func:`message`. + """ + if status == expected: + return status + raise CASeverityException(func_name, message(status)) + +def withSEVCHK(fcn): + """decorator to raise a ChannelAccessException if the wrapped + ca function does not return status = dbr.ECA_NORMAL. This + handles the common case of running :func:`PySEVCHK` for a + 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) + 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. + + if args.status != dbr.ECA_NORMAL: + return + + value = dbr.cast_args(args) + kwds = {'ftype':args.type, 'count':args.count, + '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 + 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 callable(args.usr): + args.usr(value=value, **kwds) + +## connection event handler: +def _onConnectionEvent(args): + "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()) + + +## get event handler: +def _onGetEvent(args, **kws): + """get_callback event: simply store data contents which + will need conversion to python data with _unpack().""" + # 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) + try: + entry = _get_cache_by_chid(args.chid) + except KeyError: + return + + 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: + result = memcopy(dbr.cast_args(args)) + + with entry.lock: + entry.get_results[ftype][0] = result + + +## put event handler: +def _onPutEvent(args, **kwds): + 'Put completion notification - run specified callback' + fcn = args.usr + if callable(fcn): + fcn() + + +def _onAccessRightsEvent(args): + '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) +_CB_PUTWAIT = dbr.make_callback(_onPutEvent, dbr.event_handler_args) +_CB_GET = dbr.make_callback(_onGetEvent, dbr.event_handler_args) +_CB_EVENT = dbr.make_callback(_onMonitorEvent, dbr.event_handler_args) +_CB_ACCESS = dbr.make_callback(_onAccessRightsEvent, + dbr.access_rights_handler_args) + +# Now we're ready to wrap libca functions +# +### + +# contexts +@withCA +@withSEVCHK +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] + return libca.ca_context_create(ctx) + + +def create_context(ctx=None): + """Create a new context, using the value of :data:`PREEMPTIVE_CALLBACK` + to set the context type. Note that both *context_create* and + *create_context* (which is more consistent with the Verb_Object of + the rest of the CA library) are supported. + + Parameters + ---------- + ctx : int + 0 -- No preemptive callbacks, + 1 -- use use preemptive callbacks, + None -- use value of :data:`PREEMPTIVE_CALLBACK` + + """ + context_create(ctx=ctx) + global initial_context + if initial_context is None: + initial_context = current_context() + +@withCA +def context_destroy(): + "destroy current context" + ctx = current_context() + ret = libca.ca_context_destroy() + ctx_cache = _cache.pop(ctx, None) + if ctx_cache is not None: + ctx_cache.clear() + return ret + +def destroy_context(): + "destroy current context" + return context_destroy() + +@withCA +# @withSEVCHK +def attach_context(context): + "attach to the supplied context" + return libca.ca_attach_context(context) + +@withCA +@withSEVCHK +def use_initial_context(): + """Attaches to the context created when libca is initialized. + Using this function is recommended when writing threaded programs that + using CA. + + See Also + -------- + :ref:`advanced-threads-label` in doc for further discussion. + + """ + global initial_context + ret = dbr.ECA_NORMAL + if initial_context != current_context(): + ret = libca.ca_attach_context(initial_context) + return ret + +@withCA +def detach_context(): + "detach context" + return libca.ca_detach_context() + +@withCA +def replace_printf_handler(fcn=None): + """replace the normal printf() output handler + with the supplied function (defaults to :func:`sys.stderr.write`)""" + global error_message + if fcn is None: + fcn = sys.stderr.write + error_message = ctypes.CFUNCTYPE(None, ctypes.c_char_p)(fcn) + return libca.ca_replace_printf_handler(error_message) + +@withCA +def current_context(): + "return the current context" + ctx = libca.ca_current_context() + if isinstance(ctx, ctypes.c_long): ctx = ctx.value + return ctx + +@withCA +def client_status(context, level): + """print (to stderr) information about Channel Access status, + including status for each channel, and search and connection statistics.""" + return libca.ca_client_status(context, level) + +@withCA +def flush_io(): + "flush i/o" + return libca.ca_flush_io() + +@withCA +def message(status): + """Print a message corresponding to a Channel Access status return value. + """ + return BYTES2STR(libca.ca_message(status)) + +@withCA +def version(): + """ Print Channel Access version string. + Currently, this should report '4.13' """ + return BYTES2STR(libca.ca_version()) + +@withCA +def pend_io(timeout=1.0): + """polls CA for i/o. """ + ret = libca.ca_pend_io(timeout) + try: + return PySEVCHK('pend_io', ret) + except CASeverityException: + return ret + +## @withCA +def pend_event(timeout=1.e-5): + """polls CA for events """ + ret = libca.ca_pend_event(timeout) + try: + return PySEVCHK( 'pend_event', ret, dbr.ECA_TIMEOUT) + except CASeverityException: + return ret + +@withCA +def poll(evt=1.e-5, iot=1.0): + """a convenience function which is equivalent to:: + pend_event(evt) + pend_io_(iot) + + """ + pend_event(evt) + return pend_io(iot) + +@withCA +def test_io(): + """test if IO is complete: returns True if it is""" + return (dbr.ECA_IODONE == libca.ca_test_io()) + +## create channel +@withCA +def create_channel(pvname, connect=False, auto_cb=True, callback=None): + """ create a Channel for a given pvname + + creates a channel, returning the Channel ID ``chid`` used by other + functions to identify this channel. + + Parameters + ---------- + pvname : string + the name of the PV for which a channel should be created. + connect : bool + whether to (try to) connect to PV as soon as possible. + auto_cb : bool + whether to automatically use an internal connection callback. + callback : callable or ``None`` + user-defined Python function to be called when the connection + state change s. + + Returns + ------- + chid : ctypes.c_long + channel ID. + + + Notes + ----- + 1. The user-defined connection callback function should be prepared to accept + keyword arguments of + + =========== ============================= + keyword meaning + =========== ============================= + `pvname` name of PV + `chid` Channel ID + `conn` whether channel is connected + =========== ============================= + + + 2. If `auto_cb` is ``True``, an internal connection callback is used so + that you should not need to explicitly connect to a channel, unless you + are having difficulty with dropped connections. + + 3. If the channel is already connected for the PV name, the callback + will be called immediately. + + + """ + # 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. + + 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(entry.chid) + return entry.chid + +@withCHID +def connect_channel(chid, timeout=None, verbose=False): + """connect to a channel, waiting up to timeout for a + channel to connect. It returns the connection state, + ``True`` or ``False``. + + This is usually not needed, as implicit connection will be done + when needed in most cases. + + Parameters + ---------- + chid : ctypes.c_long + Channel ID + timeout : float + maximum time to wait for connection. + verbose : bool + whether to print out debugging information + + Returns + ------- + connection_state : bool + that is, whether the Channel is connected + + Notes + ----- + 1. If *timeout* is ``None``, the value of :data:`DEFAULT_CONNECTION_TIMEOUT` is used (defaults to 2.0 seconds). + + 2. Normally, channels will connect in milliseconds, and the connection + callback will succeed on the first attempt. + + 3. For un-connected Channels (that are nevertheless queried), the 'ts' + (timestamp of last connection attempt) and 'failures' (number of failed + connection attempts) from the :data:`_cache` will be used to prevent + spending too much time waiting for a connection that may never happen. + + """ + if verbose: + write(' connect channel -> %s %s %s ' % + (repr(chid), repr(state(chid)), repr(dbr.CS_CONN))) + conn = (state(chid) == dbr.CS_CONN) + if not conn: + # not connected yet, either indicating a slow network + # or a truly un-connnectable channel. + start_time = time.time() + ctx = current_context() + pvname = name(chid) + if timeout is None: + timeout = DEFAULT_CONNECTION_TIMEOUT + + while (not conn and ((time.time()-start_time) < timeout)): + poll() + conn = (state(chid) == dbr.CS_CONN) + if not conn: + 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): + ch = get_cache(name(chid)) + + 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" + return BYTES2STR(libca.ca_name(chid)) + +@withCHID +def host_name(chid): + "return host name and port serving Channel" + return BYTES2STR(libca.ca_host_name(chid)) + +@withCHID +def element_count(chid): + """return number of elements in Channel's data. + 1 for most Channels, > 1 for waveform Channels""" + + return libca.ca_element_count(chid) + +@withCHID +def read_access(chid): + "return *read access* for a Channel: 1 for ``True``, 0 for ``False``." + return libca.ca_read_access(chid) + +@withCHID +def write_access(chid): + "return *write access* for a channel: 1 for ``True``, 0 for ``False``." + return libca.ca_write_access(chid) + +@withCHID +def field_type(chid): + "return the integer DBR field type." + return libca.ca_field_type(chid) + +@withCHID +def clear_channel(chid): + "clear the channel" + 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): + "return state (that is, attachment state) for channel" + + return libca.ca_state(chid) + +def isConnected(chid): + """return whether channel is connected: `dbr.CS_CONN==state(chid)` + + This is ``True`` for a connected channel, ``False`` for an unconnected channel. + """ + + return dbr.CS_CONN == state(chid) + +def access(chid): + """returns a string describing read/write access: one of + `no access`, `read-only`, `write-only`, or `read/write` + """ + 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.""" + 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: + ftype += dbr.TIME_STRING + if ftype == dbr.CTRL_STRING: + 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_array_get_callback` or subscription callback, and returns + the corresponding Python data + + Normally, users are not expected to need to access this function, but + it will be necessary why using :func:`sg_get`. + + Parameters + ------------ + chid : ctypes.c_long or ``None`` + channel ID (if not None, used for determining count and ftype) + data : object + raw data as returned by internal libca functions. + count : integer + number of elements to fetch (defaults to element count of chid or 1) + ftype : integer + data type of channel (defaults to native type of chid) + as_numpy : bool + whether to convert to numpy array. + """ + + def scan_string(data, count): + """ Scan a string, or an array of strings as a list, depending on content """ + out = [] + for elem in range(min(count, len(data))): + this = strjoin('', BYTES2STR(data[elem].value)).rstrip() + if NULLCHAR_2 in this: + this = this[:this.index(NULLCHAR_2)] + out.append(this) + if len(out) == 1: + out = out[0] + return out + + def array_cast(data, count, ntype, use_numpy): + "cast ctypes array to numpy array (if using numpy)" + if use_numpy: + dtype = dbr.NP_Map.get(ntype, None) + if dtype is not None: + out = numpy.empty(shape=(count,), dtype=dbr.NP_Map[ntype]) + ctypes.memmove(out.ctypes.data, data, out.nbytes) + else: + out = numpy.ctypeslib.as_array(memcopy(data)) + else: + out = memcopy(data) + return out + + def unpack(data, count, ntype, use_numpy, elem_count): + "simple, native data type" + if data is None: + return None + elif ntype == dbr.CHAR and elem_count > 1: + return array_cast(data, count, ntype, use_numpy) + elif count == 1 and ntype != dbr.STRING: + return data[0] + elif ntype == dbr.STRING: + return scan_string(data, count) + elif count != 1: + return array_cast(data, count, ntype, use_numpy) + return data + + # Grab the native-data-type data + try: + extended_data, data = data + except (TypeError, IndexError): + return None + except ValueError: + extended_data = None + + if count == 0 or count is None: + count = len(data) + else: + count = min(len(data), count) + + if ftype is None and chid is not None: + 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) + + +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. + Note that there is not a separate form for array data. + + 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 below. + 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 : object + Normally, the value of the data. Will return ``None`` if the + channel is not connected, `wait=False` was used, or the data + transfer timed out. + + Notes + ----- + 1. Returning ``None`` indicates an *incomplete get* + + 2. The *as_string* option is not as complete as the *as_string* + argument for :meth:`PV.get`. For Enum types, the name of the Enum + state will be returned. For waveforms of type CHAR, the string + representation will be returned. For other waveforms (with *count* > + 1), a string like `` will be returned. + + 3. The *as_numpy* option will convert waveform data to be returned as a + numpy array. This is only applied if numpy can be imported. + + 4. The *wait* option controls whether to wait for the data to be + received over the network and actually return the value, or to return + immediately after asking for it to be sent. If `wait=False` (that is, + immediate return), the *get* operation is said to be *incomplete*. The + data will be still be received (unless the channel is disconnected) + eventually but stored internally, and can be read later with + :func:`get_complete`. Using `wait=False` can be useful in some + circumstances. + + 5. The *timeout* option sets the maximum time to wait for the data to + be received over the network before returning ``None``. Such a timeout + could imply that the channel is disconnected or that the data size is + larger or network slower than normal. In that case, the *get* + operation is said to be *incomplete*, and the data may become available + later with :func:`get_complete`. + + """ + 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) + + +@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 + + 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 + ---------- + 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 : 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. + """ + if ftype is None: + ftype = field_type(chid) + if count is None: + count = element_count(chid) + else: + count = min(count, element_count(chid)) + + 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 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 + + 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) + + if as_string: + val = _as_string(val, chid, count, ftype) + elif isinstance(val, ctypes.Array) and HAS_NUMPY and as_numpy: + val = numpy.ctypeslib.as_array(memcopy(val)) + + # value retrieved, clear cached value + 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" + try: + if (ftype in (dbr.CHAR, dbr.TIME_CHAR, dbr.CTRL_CHAR) and + count < AUTOMONITOR_MAXLENGTH): + val = strjoin('', [chr(i) for i in val if i>0]).strip() + elif ftype == dbr.ENUM and count == 1: + val = get_enum_strings(chid)[val] + elif count > 1: + val = '' % (count, ftype) + val = str(val) + except ValueError: + pass + return val + +@withConnectedCHID +def put(chid, value, wait=False, timeout=30, callback=None, + callback_data=None): + """sets the Channel to a value, with options to either wait + (block) for the processing to complete, or to execute a + supplied callback function when the process has completed. + + + Parameters + ---------- + chid : ctypes.c_long + Channel ID + wait : bool + whether to wait for processing to complete (or time-out) + before returning. + timeout : float + maximum time to wait for processing to complete before returning anyway. + 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. + + Returns + ------- + status : int + 1 for success, -1 on time-out + + Notes + ----- + 1. Specifying a callback will override setting `wait=True`. + + 2. A put-callback function will be called with keyword arguments + pvname=pvname, data=callback_data + + """ + ftype = field_type(chid) + count = nativecount = element_count(chid) + if count > 1: + # check that data for array PVS is a list, array, or string + try: + if ftype == dbr.STRING and is_string_or_bytes(value): + # len('abc') --> 3, however this is one element for dbr.STRING ftype + count = 1 + else: + count = min(len(value), count) + + if count == 0: + count = nativecount + except TypeError: + write('''PyEpics Warning: + value put() to array PV must be an array or sequence''') + if ftype == dbr.CHAR and nativecount > 1 and is_string_or_bytes(value): + count += 1 + count = min(count, nativecount) + + # if needed (python3, especially) convert to basic string/bytes form + if is_string(value): + value = ascii_string(value) + + data = (count*dbr.Map[ftype])() + if ftype == dbr.STRING: + if is_string_or_bytes(value): + data[0].value = value + else: + for elem in range(min(count, len(value))): + data[elem].value = ascii_string(value[elem]) + elif nativecount == 1: + if ftype == dbr.CHAR: + if is_string_or_bytes(value): + if isinstance(value, bytes): + value = value.decode('ascii', 'replace') + value = [ord(i) for i in value] + [0, ] + else: + data[0] = value + else: + # allow strings (even bits/hex) to be put to integer types + if is_string(value) and isinstance(data[0], (int, )): + value = int(value, base=0) + try: + data[0] = value + except TypeError: + data[0] = type(data[0])(value) + except: + errmsg = "cannot put value '%s' to PV of type '%s'" + tname = dbr.Name(ftype).lower() + raise ChannelAccessException(errmsg % (repr(value), tname)) + + else: + if ftype == dbr.CHAR and is_string_or_bytes(value): + if isinstance(value, bytes): + value = value.decode('ascii', 'replace') + value = [ord(i) for i in value] + [0, ] + try: + ndata, nuser = len(data), len(value) + if nuser > ndata: + value = value[:ndata] + data[:nuser] = list(value) + + except (ValueError, IndexError): + errmsg = "cannot put array data to PV of type '%s'" + raise ChannelAccessException(errmsg % (repr(value))) + + # simple put, without wait or callback + 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) + 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)) + + PySEVCHK('put', ret) + poll(evt=1.e-4, iot=0.05) + if wait: + while not (completed['status'] or + (time.time()-start_time) > timeout): + poll() + if not completed['status']: + ret = -ret + return ret + + +@withMaybeConnectedCHID +def get_ctrlvars(chid, timeout=5.0, warn=True): + """return the CTRL fields for a Channel. + + Depending on the native type, the keys may include + *status*, *severity*, *precision*, *units*, enum_strs*, + *upper_disp_limit*, *lower_disp_limit*, upper_alarm_limit*, + *lower_alarm_limit*, upper_warning_limit*, *lower_warning_limit*, + *upper_ctrl_limit*, *lower_ctrl_limit* + + Notes + ----- + enum_strs will be a list of strings for the names of ENUM states. + + """ + ftype = promote_type(chid, use_ctrl=True) + 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 + + +@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*. + """ + ftype = promote_type(chid, use_time=True) + 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 + + +def get_timestamp(chid): + """return the timestamp of a Channel -- the time of last update.""" + return get_timevars(chid).get('timestamp', 0) + +def get_severity(chid): + """return the severity of a Channel.""" + return get_timevars(chid).get('severity', 0) + +def get_precision(chid): + """return the precision of a Channel. For Channels with + native type other than FLOAT or DOUBLE, this will be 0""" + if field_type(chid) in (dbr.FLOAT, dbr.DOUBLE): + return get_ctrlvars(chid).get('precision', None) + return None + +def get_enum_strings(chid): + """return list of names for ENUM states of a Channel. Returns + None for non-ENUM Channels""" + if field_type(chid) == dbr.ENUM: + return get_ctrlvars(chid).get('enum_strs', None) + return None + +## +# Default mask for subscriptions (means update on value changes +# exceeding MDEL, and on alarm level changes.) Other option is +# dbr.DBE_LOG for archive changes (ie exceeding ADEL) +DEFAULT_SUBSCRIPTION_MASK = dbr.DBE_VALUE|dbr.DBE_ALARM + +@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. + + Parameters + ----------- + chid : ctypes.c_long + channel ID + use_time : bool + 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. + If ``None``, defaults to :data:`DEFAULT_SUBSCRIPTION_MASK`. + + 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) + + The returned tuple contains *callback_ref* an *user_arg_ref* which + are references that should be kept for as long as the subscription + lives (otherwise they may be garbage collected, causing no end of + trouble). *event_id* is the id for the event (useful for clearing + a subscription). + + Notes + ----- + 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_fieldtype(ftype, use_time=use_time, use_ctrl=use_ctrl) + uarg = ctypes.py_object(callback) + evid = ctypes.c_void_p() + poll() + ret = libca.ca_create_subscription(ftype, count, chid, mask, + _CB_EVENT, uarg, ctypes.byref(evid)) + PySEVCHK('create_subscription', ret) + + poll() + return (_CB_EVENT, uarg, evid) + +@withCA +@withSEVCHK +def clear_subscription(event_id): + "cancel subscription given its *event_id*" + return libca.ca_clear_subscription(event_id) + +@withCA +@withSEVCHK +def sg_block(gid, timeout=10.0): + "block for a synchronous group to complete processing" + return libca.ca_sg_block(gid, timeout) + +@withCA +def sg_create(): + """create synchronous group. + Returns a *group id*, `gid`, which is used to identify this group and + to be passed to all other synchronous group commands. + """ + gid = ctypes.c_ulong() + pgid = ctypes.pointer(gid) + ret = libca.ca_sg_create(pgid) + PySEVCHK('sg_create', ret) + return gid + +@withCA +@withSEVCHK +def sg_delete(gid): + "delete a synchronous group" + return libca.ca_sg_delete(gid) + +@withCA +def sg_test(gid): + "test whether a synchronous group has completed." + ret = libca.ca_sg_test(gid) + return PySEVCHK('sg_test', ret, dbr.ECA_IODONE) + +@withCA +@withSEVCHK +def sg_reset(gid): + "resets a synchronous group" + return libca.ca_sg_reset(gid) + +def sg_get(gid, chid, ftype=None, as_numpy=True, as_string=True): + """synchronous-group get of the current value for a Channel. + same options as get() + + This function will not immediately return the value, of course, but the + address of the underlying data. + + After the :func:`sg_block` has completed, you must use :func:`_unpack` + to convert this data address to the actual value(s). + + Examples + ======== + + >>> chid = epics.ca.create_channel(PV_Name) + >>> epics.ca.connect_channel(chid1) + >>> sg = epics.ca.sg_create() + >>> data = epics.ca.sg_get(sg, chid) + >>> epics.ca.sg_block(sg) + >>> print epics.ca._unpack(data, chid=chid) + + """ + if not isinstance(chid, dbr.chid_t): + raise ChannelAccessException("not a valid chid!") + + if ftype is None: + ftype = field_type(chid) + count = element_count(chid) + + data = (count*dbr.Map[ftype])() + ret = libca.ca_sg_array_get(gid, ftype, count, chid, data) + PySEVCHK('sg_get', ret) + poll() + + val = _unpack(chid, data, count=count, ftype=ftype, as_numpy=as_numpy) + if as_string: + val = _as_string(val, chid, count, ftype) + return val + +def sg_put(gid, chid, value): + """perform a `put` within a synchronous group. + + This `put` cannot wait for completion or for a a callback to complete. + """ + if not isinstance(chid, dbr.chid_t): + raise ChannelAccessException("not a valid chid!") + + ftype = field_type(chid) + count = element_count(chid) + data = (count*dbr.Map[ftype])() + + if ftype == dbr.STRING: + if count == 1: + data[0].value = value + else: + for elem in range(min(count, len(value))): + data[elem].value = value[elem] + elif count == 1: + try: + data[0] = value + except TypeError: + data[0] = type(data[0])(value) + except: + errmsg = "Cannot put value '%s' to PV of type '%s'" + tname = dbr.Name(ftype).lower() + raise ChannelAccessException(errmsg % (repr(value), tname)) + + else: + # auto-convert strings to arrays for character waveforms + # could consider using + # numpy.fromstring(("%s%s" % (s,NULLCHAR*maxlen))[:maxlen], + # dtype=numpy.uint8) + if ftype == dbr.CHAR and is_string_or_bytes(value): + pad = [0]*(1+count-len(value)) + if isinstance(value, bytes): + value = value.decode('ascii', 'replace') + value = ([ord(i) for i in value] + pad)[:count] + + try: + ndata = len(data) + nuser = len(value) + if nuser > ndata: + value = value[:ndata] + data[:len(value)] = list(value) + except: + errmsg = "Cannot put array data to PV of type '%s'" + raise ChannelAccessException(errmsg % (repr(value))) + + ret = libca.ca_sg_array_put(gid, ftype, count, chid, data) + PySEVCHK('sg_put', ret) + # poll() + return ret + +class CAThread(threading.Thread): + """ + Sub-class of threading.Thread to ensure that the + initial CA context is used. + """ + def run(self): + use_initial_context() + threading.Thread.run(self) diff --git a/epics/compat/CaChannel.py b/epics/compat/CaChannel.py new file mode 100644 index 0000000..fd84ec5 --- /dev/null +++ b/epics/compat/CaChannel.py @@ -0,0 +1,539 @@ +""" +Port of Xiaogiang Wang's CaChannel class to use epics.ca + +Matt Newville 20-October-2010 + +Original Comments: + +CaChannel class having identical API as of caPython/CaChannel class, +based on PythonCA ( > 1.20.1beta2) + +Author: Xiaoqiang Wang +Created: Sep. 22, 2008 +Changes: +""" + +from epics import ca, dbr +from epics.wx import closure +import time +import types + + +code = ''' +class CaChannelException(Exception): + def __init__(self, status): + self.status = str(status) + def __str__(self): + return self.status + +class CaChannel(object): + """CaChannel: A Python class with identical API as of caPython/CaChannel + + Example: + import CaChannel + chan = CaChannel.CaChannel('catest') + chan.searchw() + print chan.getw() + """ + ca_timeout = 1.0 + + def __init__(self, pvName=None): + self.pvname = pvName + self.__chid = None + self.__evid = None + self.__timeout = None + self._field_type = None + self._element_count = None + self._puser = None + self._conn_state = None + self._host_name = None + self._raccess = None + self._waccess = None + + self._callbacks={} + + def __del__(self): + try: + self.clear_event() + self.clear_channel() + self.flush_io() + except: + pass + + def version(self): + print("CaChannel, version v03 (pyepics port of v02-11-09)") +# +# Class helper methods +# + def setTimeout(self, timeout): + """Set the timeout for this channel.""" + if (timeout>=0 or timeout == None): + self.__timeout = timeout + else: + raise ValueError + def getTimeout(self): + """Retrieve the timeout set for this channel.""" + return self.__timeout + + +# +# *************** Channel access medthod *************** +# + +# +# Connection methods +# search_and_connect +# search +# clear_channel + + def search_and_connect(self, pvName, callback, *user_args): + """Attempt to establish a connection to a process variable. + Parameters: + pvName: process variable name + callback: function called when connection completes and connection + status changes later on. + *user_args: user provided arguments that are passed to callback when + it is invoked. + """ + if pvName == None: + pvName = self.pvname + conn_callback = closure(callback, *user_args) + try: + self.__chid = ca.create_channel(pvName, callback=conn_callback) + except ca.ChannelAccessException, msg: + raise CaChannelException(msg) + + def search(self, pvName=None): + """Attempt to establish a connection to a process variable. + Parameters: + pvName: process variable name + """ + if pvName == None: + pvName = self.pvname + try: + self.__chid = ca.create_channel(pvName) + except ca.ChannelAccessException, msg: + raise CaChannelException, msg + + def clear_channel(self): + """Close a channel created by one of the search functions""" + if(self.__chid is not None): + try: + status = ca.clear_channel(self.__chid) + except ca.ChannelAccessException, msg: + raise CaChannelException,msg + +# +# Write methods +# array_put +# array_put_callback +# + + def _setup_put(self, value, req_type, count = None): + if count is None: + count = self.element_count() + else: + count = max(1, min(self.element_count(), count) ) + + if req_type == -1: + req_type = self.field_type() + + # single numeric value + if (isinstance(value, int) or + isinstance(value, long) or + isinstance(value, float) or + isinstance(value, bool)): + pval = (CaChannel.dbr_d[req_type](value),) + # single string value + # if DBR_CHAR, split into chars + # otherwise convert to field type + elif isinstance(value, str): + if req_type == ca.DBR_CHAR: + if len(value) < count: + count = len(value) + pval = [ord(x) for x in value[:count]] + else: + pval = (CaChannel.dbr_d[req_type](value),) + # assumes other sequence type + else: + if len(value) < count: + count = len(value) + pval = [CaChannel.dbr_d[req_type](x) for x in value[:count]] + + return pval + + def array_put(self, value, req_type=None, count=None): + """Write a value or array of values to a channel + Parameters: + value: data to be written. For multiple values use a list or tuple + req_type: database request type. Defaults to be the native data type. + count: number of data values to write, Defaults to be the native count. + """ + if req_type is None: req_type = -1 + val = self._setup_put(value, req_type, count) + try: + ca.put(self.__chid, val, None, None, req_type) + except ca.ChannelAccessException,msg: + raise CaChannelException,msg + + def array_put_callback(self, value, req_type, count, callback, *user_args): + """Write a value or array of values to a channel and execute the user + supplied callback after the put has completed. + Parameters: + value: data to be written. For multiple values use a list or tuple. + req_type: database request type. Defaults to be the native data type. + count: number of data values to write, Defaults to be the native count. + callback: function called when the write is completed. + *user_args: user provided arguments that are passed to callback when + it is invoked. + """ + if req_type is None: req_type = -1 + val = self._setup_put(value, req_type, count) + self._callbacks['putCB']=(callback, user_args) + try: + ca.put(self.__chid, val, None, self._put_callback, req_type) + except ca.ChannelAccessException,msg: + raise CaChannelException,msg +# +# Read methods +# getValue +# array_get +# array_get_callback +# + + # Obtain read value after ECA_NORMAL is returned on an array_get(). + def getValue(self): + """Return the value(s) after array_get has completed""" + return self.val + + # Simulate with a synchronous getw function call + def array_get(self, req_type=None, count=None): + """Read a value or array of values from a channel. The new value is + retrieved by a call to getValue method. + Parameters: + req_type: database request type. Defaults to be the native data type. + count: number of data values to read, Defaults to be the native count. + """ + self.val = self.getw(req_type, count) + + def array_get_callback(self, req_type, count, callback, *user_args): + """Read a value or array of values from a channel and execute the user + supplied callback after the get has completed. + Parameters: + req_type: database request type. Defaults to be the native data type. + count: number of data values to read, Defaults to be the native count. + callback: function called when the get is completed. + *user_args: user provided arguments that are passed to callback when + it is invoked. + """ + if req_type is None: req_type = -1 + if count is None: count = 0 + self._callbacks['getCB']=(callback, user_args) + try: + status=ca.get(self.__chid, self._get_callback, req_type, count) + except ca.ChannelAccessException,msg: + raise CaChannelException,msg + +# +# Event methods +# add_masked_array_event +# clear_event +# + + # Creates a new event id and stores it on self.__evid. Only one event registered + # per CaChannel object. If an event is already registered the event is cleared + # before registering a new event. + def add_masked_array_event(self, req_type, count, mask, callback, *user_args): + """Specify a callback function to be executed whenever changes occur to a PV. + Parameters: + req_type: database request type. Defaults to be the native data type. + count: number of data values to read, Defaults to be the native count. + mask: logical or of ca.DBE_VALUE, ca.DBE_LOG, ca.DBE_ALARM. Defaults to + be ca.DBE_VALUE|ca.DBE_ALARM. + callback: function called when the get is completed. + *user_args: user provided arguments that are passed to callback when + it is invoked. + """ + if req_type is None: req_type = -1 + if count is None: count = 0 + if mask is None: mask = ca.DBE_VALUE|ca.DBE_ALARM + if self.__evid is not None: + self.clear_event() + self.pend_io() + self._callbacks['eventCB']=(callback, user_args) + try: + self.__evid = ca.monitor(self.__chid, self._event_callback, count, mask) + except ca.ChannelAccessException,msg: + raise CaChannelException,msg + + def clear_event(self): + """Remove previously installed callback function.""" + if self.__evid is not None: + try: + status=ca.clear_monitor(self.__evid) + self.__evid = None + except ca.ChannelAccessException,msg: + raise CaChannelException,msg + +# +# Execute methods +# pend_io +# pend_event +# poll +# flush_io +# + + def pend_io(self,timeout=None): + """Flush the send buffer and wait until outstanding queries complete + or the specified timeout expires. + Parameters: + timeout: seconds to wait + """ + if timeout is None: + if self.__timeout is None: + timeout = self.ca_timeout + else: + timeout = self.__timeout + status = ca.pend_io(float(timeout)) + if status != 0: + raise CaChannelException, ca.caError._caErrorMsg[status] + + def pend_event(self,timeout=None): + """Flush the send buffer and wait for timeout seconds. + Parameters: + timeout: seconds to wait + """ + if timeout is None: + timeout = 0.1 + status = ca.pend_event(timeout) + # status is always ECA_TIMEOUT + return status + + def poll(self): + """Flush the send buffer and execute outstanding background activity.""" + status = ca.poll() + # status is always ECA_TIMEOUT + return status + + def flush_io(self): + """Flush the send buffer and does not execute outstanding background activity.""" + status = ca.flush() + if status != 0: + raise CaChannelException, ca.caError._caErrorMsg[status] + +# +# Channel Access Macros +# field_type +# element_count +# name +# state +# host_name +# read_access +# write_access +# + def get_info(self): + try: + info=(self._field_type, self._element_count, self._puser, + self._conn_state, self._host_name, self._raccess, + self._waccess) = ca.ch_info(self.__chid) + except ca.ChannelAccessException,msg: + raise CaChannelException,msg + return info + + def field_type(self): + """Native field type.""" + self.get_info() + return self._field_type + + def element_count(self): + """Native element count.""" + self.get_info() + return self._element_count + + def name(self): + """Channel name specified when the channel was connected.""" + return ca.name(self.__chid) + + def state(self): + """Current state of the connections. + Possible channel states: + ca.cs_never_conn PV not found + ca.cs_prev_conn PV was found but unavailable + ca.cs_conn PV was found and available + ca.cs_closed PV not closed + ca.cs_never_search PV not searched yet + """ + if self.__chid is None: + return dbr.CS_NEVER_SEARCH + else: + self.get_info() + return self._conn_state + + def host_name(self): + """Host name that hosts the process variable.""" + self.get_info() + return self._host_name + + def read_access(self): + """Right to read the channel.""" + self.get_info() + return self._raccess + + def write_access(self): + """Right to write the channel.""" + self.get_info() + return self._waccess +# +# Wait functions +# +# These functions wait for completion of the requested action. + def searchw(self, pvName=None): + """Attempt to establish a connection to a process variable. + Parameters: + pvName: process variable name + """ + if pvName is None: + pvName = self.pvname + self.__chid = ca.search(pvName, None) + if self.__timeout is None: + timeout = self.ca_timeout + else: + timeout = self.__timeout + status = ca.pend_io(timeout) + if status != 0: + raise CaChannelException, ca.caError._caErrorMsg[status] + + def putw(self, value, req_type=None): + """Write a value or array of values to a channel + Parameters: + value: data to be written. For multiple values use a list or tuple + req_type: database request type. Defaults to be the native data type. + """ + if req_type is None: req_type = -1 + val = self._setup_put(value, req_type) + ca.put(self.__chid, val, None, None, req_type) + if self.__timeout is None: + timeout = self.ca_timeout + else: + timeout = self.__timeout + status = ca.pend_io(timeout) + if status != 0: + raise CaChannelException, ca.caError._caErrorMsg[status] + + def getw(self, req_type=None, count=None): + """Return a value or array of values from a channel. + Parameters: + req_type: database request type. Defaults to be the native data type. + count: number of data values to read, Defaults to be the native count. + """ + updated = [False] + value = [0] + def update_value(valstat): + if valstat is None: + return + try: + value[0] = valstat[0] + finally: + updated[0] = True + if req_type is None: req_type = -1 + if count is None: count = 0 + ca.get(self.__chid, update_value, req_type, count) + if self.__timeout is None: + timeout = self.ca_timeout + else: + timeout = self.__timeout + status = ca.pend_io(timeout) + n = 0 + while n*0.02 < timeout and not updated[0]: + ca.pend_event(0.02) + n+=1 + if not updated[0]: + raise CaChannelException, ca.caError._caErrorMsg[10] # ECA_TIMEOUT + return value[0] + +# +# Callback functions +# +# These functions hook user supplied callback functions to CA extension + + def _conn_callback(self): + try: + callback, userArgs = self._callbacks.get('connCB') + except: + return + if self.state() == 2: OP=6 + else: OP=7 + epicsArgs = (self.__chid, OP) + callback(epicsArgs, userArgs) + + def _put_callback(self, args): + try: + callback, userArgs = self._callbacks.get('putCB') + except: + return + epicsArgs={} + epicsArgs['chid']=self.__chid + epicsArgs['type']=self.field_type() + epicsArgs['count']=self.element_count() + epicsArgs['status']=args[1] + callback(epicsArgs, userArgs) + + def _get_callback(self, args): + try: + callback, userArgs = self._callbacks.get('getCB') + except: + return + epicsArgs = self._format_cb_args(args) + callback(epicsArgs, userArgs) + + def _event_callback(self, args): + try: + callback, userArgs = self._callbacks.get('eventCB') + except: + return + epicsArgs = self._format_cb_args(args) + callback(epicsArgs, userArgs) + + def _format_cb_args(self, args): + epicsArgs={} + epicsArgs['chid'] = self.__chid + # dbr_type is not returned + # use dbf_type instead + epicsArgs['type'] = self.field_type() + epicsArgs['count'] = self.element_count() + # status flag is not returned, + # args[1] is alarm status + # assume ECA_NORMAL + epicsArgs['status'] = 1 + if len(args)==2: # Error + epicsArgs['pv_value'] = args[0] # always None + epicsArgs['status'] = args[1] + if len(args)>=3: # DBR_Plain + epicsArgs['pv_value'] = args[0] + epicsArgs['pv_severity']= args[1] + epicsArgs['pv_status'] = args[2] + if len(args)>=4: # DBR_TIME, 0.0 for others + epicsArgs['pv_seconds'] = args[3] + if len(args)==5: + if len(args[4])==2: # DBR_CTRL_ENUM + epicsArgs['pv_nostrings'] = args[4][0] + epicsArgs['pv_statestrings']= args[4][1] + if len(args[4])>=7: # DBR_GR + epicsArgs['pv_units'] = args[4][0] + epicsArgs['pv_updislim'] = args[4][1] + epicsArgs['pv_lodislim'] = args[4][2] + epicsArgs['pv_upalarmlim'] = args[4][3] + epicsArgs['pv_upwarnlim'] = args[4][4] + epicsArgs['pv_loalarmlim'] = args[4][5] + epicsArgs['pv_lowarnlim'] = args[4][6] + if len(args[4])==8: # DBR_GR_FLOAT or DBR_GR_DOUBLE + epicsArgs['pv_precision'] = args[4][7] + if len(args[4])>=9: # DBR_CTRL + epicsArgs['pv_upctrllim'] = args[4][7] + epicsArgs['pv_loctrllim'] = args[4][8] + if len(args[4])==10: # DBR_CTRL_FLOAT or DBR_CTRL_DOUBLE + epicsArgs['pv_precision'] = args[4][9] + return epicsArgs + +''' + diff --git a/epics/compat/__init__.py b/epics/compat/__init__.py new file mode 100644 index 0000000..2e76d76 --- /dev/null +++ b/epics/compat/__init__.py @@ -0,0 +1,5 @@ +""" +compatibility with other Epics CA implementations +""" +from .epicsPV import epicsPV + diff --git a/epics/compat/ca_util.py b/epics/compat/ca_util.py new file mode 100644 index 0000000..71372d8 --- /dev/null +++ b/epics/compat/ca_util.py @@ -0,0 +1,659 @@ +#!/usr/bin/env python + +"""ca_util.py is a wrapper around pyepics that allows the caller to write, +e.g., + caget("xxx:m1") +instead of having to write + m1 = CaChannel() + m1.searchw("xxx:m1") + m1.getw() +Also, ca_util defends against null PV names and some effects of short-term +CA disconnections, and it can verify that caput*() operations succeeded. + +This is a port of Tim Mooney's wrapper around CaChannel to pyepics. +""" + +# Port of Tim Mooney's wrapper around CaChannel to pyepics +# Tim Mooney 12/05/2008 + +code = ''' +version = "3.0" + +import epics +import time +import sys + +# DBR types +# ca.DBR_STRING = 0 +# ca.DBR_SHORT = 1 +# ca.DBR_INT = 1 +# ca.DBR_FLOAT = 2 +# ca.DBR_ENUM = 3 +# ca.DBR_CHAR = 4 +# ca.DBR_LONG = 5 +# ca.DBR_DOUBLE = 6 + +####################################################################### +# Human readable exception description +# try: +# x = x + 1 +# except: +# print formatExceptionInfo() +import sys +import traceback +def formatExceptionInfo(maxTBlevel=5): + cla, exc, trbk = sys.exc_info() + excName = cla.__name__ + try: + excArgs = exc.__dict__["args"] + except KeyError: + excArgs = "" + excTb = traceback.format_tb(trbk, maxTBlevel) + return (excName, excArgs, excTb) + +####################################################################### +# channel-access connection states +ca_states = {} +# ...from cadef.h: +ca_states[ca.cs_never_conn] = "never connected" +ca_states[ca.cs_prev_conn] = "previously connected" +ca_states[ca.cs_conn] = "connected" +ca_states[ca.cs_closed] = "closed" +# ...from CaChannel.py (only with the patch from 04/29/04): +try: + ca_states[ca.cs_never_search] = "never searched" +except AttributeError: + file = getCaChannelFileName() + using_old_CaChannel = 1 + print "ca_util: You're using an old version of CaChannel (%s)." % getCaChannelFileName() + pass + + + +####################################################################### +# default settings for ca_util +defaultTimeout = None # 'None' means use CaChannel's timeout +defaultRetries = 3 +readCheckTolerance = None # 'None" means don't check + +def set_ca_util_defaults(timeout=None, retries=None, read_check_tolerance=None): + """ + usage: old = set_ca_util_defaults(timeout=None, retries=None, + read_check_tolerance=None) + alternate: set_ca_util_defaults(defaultsList), where defaultsList is like + the list returned by get_ca_util_defaults() + Setting an argument to the string "NONE" disables it. + Returns the list of previous default values: + [defaultTimeout, defaultRetries, readCheckTolerance] + """ + global defaultTimeout, defaultRetries, readCheckTolerance + old = [defaultTimeout, defaultRetries, readCheckTolerance] + if type(timeout) == type([]): + argList = timeout + timeout = argList[0] + retries = argList[1] + read_check_tolerance = argList[2] + if (timeout!=None) : defaultTimeout = timeout + if (retries!=None) : defaultRetries = retries + if (read_check_tolerance!=None) : readCheckTolerance = read_check_tolerance + return old + +def get_ca_util_defaults(): + """ + usage: myList = get_ca_util_defaults() + myList is set to [defaultTimeout, defaultRetries, readCheckTolerance] + """ + global defaultTimeout, defaultRetries, readCheckTolerance + return [defaultTimeout, defaultRetries, readCheckTolerance] + +def set_ca_util_default_timeout(timeout=None): + """ + usage: old = set_ca_util_default_timeout(timeout=None) + If timeout == "NONE", then ca_util doesn't specify any timeout in + calls to underlying software. + Returns previous default timeout. + """ + global defaultTimeout + old = defaultTimeout + defaultTimeout = timeout + return old + +def get_ca_util_default_timeout(): + global defaultTimeout + return defaultTimeout + +def set_ca_util_default_retries(retries=None): + """ + usage: old = set_ca_util_default_retries(retries=None) + If retries == "NONE", then ca_util doesn't do any retries. + Returns previous default retries. + """ + global defaultRetries + old = defaultRetries + defaultRetries = retries + return old + +def get_ca_util_default_retries(): + global defaultRetries + return defaultRetries + +def set_ca_util_default_read_check_tolerance(read_check_tolerance=None): + """ + usage: old = set_ca_util_default_read_check_tolerance(read_check_tolerance=None) + If read_check_tolerance == "NONE", then ca_util doesn't compare the value + it reads to the value it wrote. + Returns previous default tolerance. + """ + global readCheckTolerance + old = readCheckTolerance + readCheckTolerance = read_check_tolerance + return old + +def get_ca_util_default_read_check_tolerance(): + global readCheckTolerance + return readCheckTolerance + + +####################################################################### +# The dictionary, cadict, will be used to associate PV names with the +# machinery required to talk to EPICS PV's. If no entry is found (the +# name hasn't been used yet in a ca call), then we create a new instance +# of CaChannel, connect it to the PV, and put it in the dictionary. We also +# include a flag some of the ca_util routines can use to check if a callback +# has occurred for this PV. + +class cadictEntry: + def __init__(self, channel): + self.channel = channel + self.callbackReceived = 0 # reserved for use by caputw() + self.field_type = channel.field_type() + self.element_count = channel.element_count() + #self.host_name = channel.host_name() + +cadict = {} + +####################################################################### +ca_utilExceptionStrings = ["No name was provided.", "Readback disagrees with put value.", + "PV is not connected."] +EXCEPTION_NULL_NAME = 0 +EXCEPTION_READBACK_DISAGREES = 1 +EXCEPTION_NOT_CONNECTED = 2 + +class ca_utilException(Exception): + def __init__(self, *args): + Exception.__init__(self, *args) + self.errorNumber = args[0] + + def __int__(self): + return int(self.errorNumber) + + def __str__(self): + return ca_utilExceptionStrings[self.errorNumber] + + +####################################################################### +def convertToType(type, value): + if type == ca.DBR_STRING: + return str(value) + elif type == ca.DBR_SHORT or type == ca.DBR_INT or type == ca.DBR_LONG: + try: + n = int(value) + except: + n = 0 + return n + elif type == ca.DBR_FLOAT or type == ca.DBR_DOUBLE: + try: + n = float(value) + except: + n = 0.0 + return n + elif type == ca.DBR_ENUM: + return value + elif type == ca.DBR_CHAR: + return value + else: + return value + +####################################################################### +def checkName(name, timeout=None, retries=None): + """ + usage: checkName("xxx:m1.VAL", timeout=None, retries=None) + Intended for internal use by ca_util functions. + """ + + global cadict, defaultTimeout, defaultRetries + if not name: + raise ca_utilException, EXCEPTION_NULL_NAME + return + + if ((timeout == None) and (defaultTimeout != None)): timeout = defaultTimeout + if (timeout == "NONE"): timeout = None + + if ((retries == None) and (defaultRetries != None)): retries = defaultRetries + if ((retries == None) or (retries == "NONE")): retries = 0 + + tries = 0 + while (not cadict.has_key(name)) and (tries <= retries): + # Make a new entry in the PV-name dictionary + try: + channel = CaChannel.CaChannel() + if (timeout != None): channel.setTimeout(timeout) + channel.searchw(name) + cadict[name] = cadictEntry(channel) + except CaChannel.CaChannelException, status: + del channel + tries += 1 + + if (not cadict.has_key(name)): + print "ca_util.checkName: Can't connect to '%s'" % name + raise CaChannel.CaChannelException, status + +####################################################################### +def castate(name=None, timeout=None, retries=None): + """usage: val = castate("xxx:m1.VAL", timeout=None, retries=None) + Try to read a PV, to find out whether it's really connected, and + whether caller is permitted to read and write it, without allowing + any exceptions to be thrown at the caller. + """ + + global cadict, defaultTimeout, defaultRetries + + if not name: return "Null name has no state" + + # The only reliable way to check the *current* state of a PV is to attempt to use it. + try: + val = caget(name, timeout=timeout, retries=retries) + except CaChannel.CaChannelException, status: + pass + + try: + checkName(name, timeout=timeout) + except CaChannel.CaChannelException, status: + return "not connected" + except: + return "error" + + try: + state = cadict[name].channel.state() + except CaChannel.CaChannelException, status: + return "not connected" + except: + return "error" + else: + try: + read_access = cadict[name].channel.read_access() + write_access = cadict[name].channel.write_access() + if ca_states.has_key(state): + s = ca_states[state] + else: + s = "unknown state" + if not read_access: s += ", noread" + if not write_access: s += ", nowrite" + return s + except: + return "error" + +####################################################################### +def caget(name, timeout=None, retries=None, req_type=None, req_count=None): + """usage: val = caget("xxx:m1.VAL", timeout=None, retries=None, + req_type=None, req_count=None)""" + + global cadict, defaultTimeout, defaultRetries + + if not name: + print "caget: no PV name supplied" + raise ca_utilException, EXCEPTION_NULL_NAME + return 0 + if ((timeout==None) and (defaultTimeout != None)): timeout = defaultTimeout + if (timeout == "NONE"): timeout = None + if ((retries==None) and (defaultRetries != None)): retries = defaultRetries + if ((retries == None) or (retries == "NONE")): retries = 0 + retries = max(retries,0) + retry = retries + 1 + success = 0 + + # CaChannel sometimes chokes when it tries to process a channel that has been disconnected. + # The simplest fix is to clear the channel and reconnect to the PV, which we can do cleanly + # by deleting our dict entry for the channel, and calling checkName() to make a new entry. + + while ((not success) and (retry > 0)): + checked = 0 + while ((not checked) and (retry > 0)): + retry -= 1 + try: + checkName(name, timeout=timeout) + except CaChannel.CaChannelException, status: + if retry <= 0: + raise CaChannel.CaChannelException, status + return 0 + else: + checked = 1 + + entry = cadict[name] + if (timeout != None): entry.channel.setTimeout(timeout) + if req_type == None: + req_type=entry.field_type + # kludge for broken DBR_CHAR + if req_type == ca.DBR_CHAR: + req_type = ca.DBR_INT + if req_count == None: + req_count = entry.element_count + req_count = max(0, min(req_count, entry.element_count)) + try: + if (using_old_CaChannel): + val = entry.channel.getw(req_type=req_type) + else: + val = entry.channel.getw(req_type=req_type, count=req_count) + except CaChannel.CaChannelException, status: + #print "getw threw an exception (%s)" % status + if ((int(status) == ca.ECA_BADTYPE) or (int(status) == ca.ECA_DISCONN)): + # Delete dictionary entry. This clears the CA connection. + print "caget: Repairing CA connection to ", name + del cadict[name] + retry += 1 + if retry <= 0: + raise CaChannel.CaChannelException, status + return 0 + else: + success = 1 + return val + +def isNumber(s): + try: + n = int(s) + except: + return False + return True + +####################################################################### +def same(value, readback, native_readback, field_type, read_check_tolerance): + """For internal use by ca_util""" + #print "ca_util.same(): field_type=%s" % field_type + #print "ca_util.same(): value='%s'; readback='%s', native_readback='%s'" % (str(value), str(readback), str(native_readback)) + #print "ca_util.same(): type(value)=%s; type(readback)=%s, type(native_readback)=%s" % (type(value), + # type(readback), type(native_readback)) + + if field_type in [ca.DBR_FLOAT, ca.DBR_DOUBLE]: + return (abs(float(readback)-float(value)) < read_check_tolerance) + elif field_type in [ca.DBR_INT, ca.DBR_SHORT, ca.DBR_LONG]: + return (abs(int(readback)-int(value)) == 0) + elif field_type == ca.DBR_ENUM: + if str(value) == str(readback): + return True + if str(value) == str(native_readback): + return True + return False + else: + return (str(value) == str(readback)) + +####################################################################### +def caput(name, value, timeout=None, req_type=None, retries=None, read_check_tolerance=None): + """ + usage: caput("xxx:m1.VAL", new_value, timeout=None, req_type=None, + retries=None, read_check_tolerance=None) + Put a value, and optionally check that the value arrived safely. + If read_check_tolerance == None (or is not supplied) then the default + read-check tolerance is used. If read_check_tolerance == "NONE", then no + read check is done. + If read_check_tolerance != "NONE", then floating point numbers must be + closer than the tolerance, and other types must agree exactly. + Note that defaults for timeout, retries, and read_check_tolerance can be + set for all ca_util functions, using the command set_ca_util_defaults(). + """ + + _caput("caput", name, value, 0, timeout, req_type, retries, read_check_tolerance) + + +####################################################################### +def __ca_util_waitCB(epics_args, user_args): + """Function for internal use by caputw().""" + #print "__ca_util_waitCB: %s done\n" % user_args[0] + cadict[user_args[0]].callbackReceived = 1 + +####################################################################### +def caputw(name, value, wait_timeout=None, timeout=None, req_type=None, retries=None, + read_check_tolerance=None): + """ + usage: caputw("xxx:m1.VAL", new_value, wait_timeout=None, timeout=None, + req_type=None, retries=None, read_check_tolerance=None) + Put a value, optionally check that the value arrived safely, and wait (no + longer than wait_timeout) for processing to complete. If + read_check_tolerance == None (or is not supplied) then the default + read-check tolerance is used. If read_check_tolerance == "NONE", then no + read check is done. If read_check_tolerance != "NONE", then floating point + numbers must be closer than the tolerance, and other types must agree + exactly. Note that defaults for timeout, retries, and read_check_tolerance + can be set for all ca_util functions, using the command + set_ca_util_defaults(). + """ + + _caput("caputw", name, value, wait_timeout, timeout, req_type, retries, read_check_tolerance) + + +####################################################################### +def _caput(function, name, value, wait_timeout=None, timeout=None, req_type=None, retries=None, read_check_tolerance=None): + + global cadict, defaultTimeout, defaultRetries, readCheckTolerance + + #print function + if not name: + print "%s: no PV name supplied" % function + raise ca_utilException, EXCEPTION_NULL_NAME + return + if ((timeout == None) and (defaultTimeout != None)): timeout = defaultTimeout + if ((retries == None) and (defaultRetries != None)): retries = defaultRetries + if ((retries == None) or (retries == "NONE")): retries = 0 + if ((read_check_tolerance == None) and (readCheckTolerance != None)): + read_check_tolerance = readCheckTolerance + + retries = max(retries,0) + retry = retries + 1 + success = 0 + + checkName(name, timeout=timeout, retries=retries) + + while ((not success) and (retry > 0)): + + retry -= 1 + entry = cadict[name] + + state = castate(name, timeout) + #print "%s: state='%s'" % (function, state) + if (state != 'connected'): + print "%s: Repairing CA connection to '%s'" % (function, name) + del cadict[name] + retry += 1 + else: + if req_type == None: + req_type=entry.field_type + if ((timeout != None) and (timeout != "NONE")): entry.channel.setTimeout(timeout) + entry.callbackReceived = 0 # in case we're doing caputw() + #value = convertToType(value, req_type) + try: + if function == "caput": + entry.channel.putw(value, req_type=req_type) + else: #caputw + retval = entry.channel.array_put_callback(value,req_type,entry.element_count,__ca_util_waitCB,name) + except CaChannel.CaChannelException, status: + print "put() threw an exception (%s)" % status + if ((int(status) == ca.ECA_BADTYPE) or (int(status) == ca.ECA_DISCONN)): + # Delete dictionary entry. This clears the CA connection. + print "%s: Repairing CA connection to '%s'" % (function, name) + del cadict[name] + retry += 1 + if retry <= 0: + raise CaChannel.CaChannelException, status + entry.callbackReceived = 1 + return + else: + if ((read_check_tolerance == None) or (read_check_tolerance == "NONE")): + success = True + else: + if timeout: + ca.pend_io(timeout) + else: + ca.pend_io(1.0) + readback_success = False + count = 0 + while ((not readback_success) and (count < retries+1)): + try: + readback = caget(name, req_type=req_type) + native_readback = caget(name) + readback_success = True + if same(value, readback, native_readback, entry.field_type, read_check_tolerance): + success = True + #print "%s: Success\n" % (function) + else: + print "%s: readback '%s' disagrees with the value '%s' we wrote." % (function, readback, value) + raise ca_utilException, EXCEPTION_READBACK_DISAGREES + entry.callbackReceived = 1 + except CaChannel.CaChannelException, status: + print "%s: exception during readback." % (function) + count += 1 + + if success and (function == "caputw"): + start_time = time.time() + timed_out = 0 + while (not entry.callbackReceived) and (not timed_out): + #print "waiting for ", name + time.sleep(0.1) + #ca.pend_io(0.1) + ca.poll() + if (not wait_timeout): + timed_out = 0 + else: + timed_out = ((time.time()-start_time) > wait_timeout) + + if not entry.callbackReceived: + print "Execution not completed by wait_timeout (%d seconds)" % wait_timeout + +####################################################################### +def camonitor(name, function, user_args=None, timeout=None, retries=None): + """ + usage: camonitor("xxx:m1.VAL", python_function, user_args, timeout=None, + retries=None) + Don't forget to call ca.pend_event() periodically. + """ + + global defaultTimeout, defaultRetries + + if not name: + print "camonitor: no PV name supplied" + raise ca_utilException, EXCEPTION_NULL_NAME + return + if not function: + print "camonitor: no callback function supplied" + raise ca_utilException, EXCEPTION_NULL_NAME + return + if not user_args: user_args = name + if ((timeout==None) and (defaultTimeout != None)): timeout = defaultTimeout + if ((retries==None) and (defaultRetries != None)): retries = defaultRetries + if ((retries == None) or (retries == "NONE")): retries = 0 + + retries = max(retries,0) + retry = retries + 1 + success = 0 + + while ((not success) and (retry > 0)): + checked = 0 + while ((not checked) and (retry > 0)): + retry -= 1 + try: + checkName(name, timeout=timeout) + except CaChannel.CaChannelException, status: + if retry <= 0: + raise CaChannel.CaChannelException, status + return + else: + checked = 1 + + entry = cadict[name] + if ((timeout != None) and (timeout != "NONE")): entry.channel.setTimeout(timeout) + try: + entry.channel.add_masked_array_event(entry.field_type,entry.element_count,ca.DBE_VALUE, function, user_args) + except CaChannel.CaChannelException, status: + #print "add_masked_array_event threw an exception (%s)" % status + if ((int(status) == ca.ECA_BADTYPE) or (int(status) == ca.ECA_DISCONN)): + # Delete dictionary entry. This clears the CA connection. + print "camonitor: Repairing CA connection to ", name + del cadict[name] + retry += 1 + if retry <= 0: + raise CaChannel.CaChannelException, status + return 0 + else: + success = 1 + +####################################################################### +def caunmonitor(name, timeout=None): + """usage: caunmonitor("xxx:m1.VAL", timeout=None)""" + + global defaultTimeout + + if not name: + print "caunmonitor: no PV name supplied" + raise ca_utilException, EXCEPTION_NULL_NAME + return + if ((timeout==None) and (defaultTimeout != None)): timeout = defaultTimeout + + if not cadict.has_key(name): + print "ca_util has no connection to '%s'" % name + raise ca_utilException, NOCONNECTION + return + + channel = cadict[name].channel + if ((timeout != None) and (timeout != "NONE")): channel.setTimeout(timeout) + try: + channel.clear_event() + except CaChannel.CaChannelException, status: + print "caunmonitor: CaChannel exception, status=%d (%s)" % (status, ca.message(status)) + return + +####################################################################### +def test_monitor_function(epics_args, user_args): + """Example callback routine for use with camonitor().""" + print 'test_monitor_function:' + print "...epics_args: ", repr(epics_args) + print "...user_args: ", repr(user_args) + + + + + +#------------------------------------------------------------------------------------------- +# miscellaneous functions that might be useful, but haven't been integrated into the package + +####################################################################### +def endianUs(): + """ + usage: endianUs() + Returns one of "Little Endian", "Big Endian", "Unknown Endian". + """ + + from struct import pack + if pack('h', 1) == pack('=h',1): + return "Big Endian" + else: + return "Unknown Endian" + +####################################################################### +def printExceptionInfo(maxTBlevel=15): + """Intended for internal use by ca_util functions.""" + + import sys, traceback + cla, exc, trbk = sys.exc_info() + excName = cla.__name__ + try: + excArgs = exc.__dict__["args"] + except KeyError: + excArgs = "" + excTb = traceback.format_tb(trbk, maxTBlevel) + print "Unanticipated exception: %s %s\n" % (excName, excArgs) + if (len(excTb) > 0): + print "Traceback:" + for trace in excTb: + print trace, + return +''' diff --git a/epics/compat/epicsPV.py b/epics/compat/epicsPV.py new file mode 100755 index 0000000..0c1d1d5 --- /dev/null +++ b/epics/compat/epicsPV.py @@ -0,0 +1,134 @@ +""" +Port of Mark Rivers epicsPV class to use epics.PV + +Matt Newville 7-April-2010 + +Original comments: + +This module defines the epicsPV class, which adds additional features to +Geoff Savage's CaChannel class. + +Author: Mark Rivers +Created: Sept. 16, 2002. +Modifications: +""" +import epics + +class epicsPV(epics.PV): + """ + This class subclasses PV to provide a compatible API to Mark Rivers + epicsPV class + + - setMonitor() sets a generic callback routine for value change events. + Subsequent getw(), getValue() or array_get() calls will return the + value from the most recent callback, and hence do not result in any + network activity or latency. This can greatly improve performance. + + - checkMonitor() returns a flag to indicate if a callback has occured + since the last call to checkMonitor(), getw(), getValue() or + array_get(). It can be used to increase efficiency in polling + applications. + + - getControl() reads the "control" and other information from an + EPICS PV without having to use callbacks. + In addition to the PV value, this will return the graphic, control and + alarm limits, etc. + + - putWait() calls array_put_callback() and waits for the callback to + occur before it returns. This allows programs to use array_put_callback() + synchronously and without user-written callbacks. + + Created: Mark Rivers, Sept. 16, 2002. + Modifications: + """ + + def __init__(self, pvname=None, wait=True): + """ + Keywords: + pvname: + An optional name of an EPICS Process Variable. + + wait: If wait==True and pvname != None then this constructor will do + a wait for connection for the PV. If wait==0 and pvname != None + then the PV will eventually connect... + + """ + # Invoke the base class initialization + PV.__init__(self, pvname) + self.monitorState = False + if pvname is not None and wait: + self.connect() + + def _getCallback(self, pvname=None, value=None, **kw): + self.monitorState = True + + def setMonitor(self): + """ + Sets a simple callback routine for value change events + to note when a change occurs. + """ + self.monitorState = False + self.add_callback(callback=self._getCallback) + + def clearMonitor(self): + """ + Cancels the effect of a previous call to setMonitor(). + """ + self.monitorState = False + + def checkMonitor(self): + """ + Returns True to indicate if a value callback has occured, + indicating a new value is available since the last check. + Returns False if no such callback has occurred. + """ + epics.poll() + out = self.monitorState + self.monitorState = False + return out + + def getControl(self): + """ + returns a dictionary of control information for a PV + Example:: + + >>> pv = epicsPV('13IDC:m1') + >>> for field, value in pv.getControl().items(): + >>> print field, ':', value + status : 0 + severity : 0 + precision : 5 + units : mm + lower_alarm_limit : 0.0 + upper_alarm_limit : 0.0 + lower_warning_limit : 0.0 + upper_warning_limit : 0.0 + lower_disp_limit : -2.4 + upper_disp_limit : 2.4 + upper_ctrl_limit : 2.4 + lower_ctrl_limit : -2.4 + """ + epics.poll() + return self.get_ctrlvars() + + def array_get(self, count=None): + """ returns PV value """ + return self.getw(count=count) + + def getw(self, count=None): + """ returns PV value""" + return self.get(count=count) + + def getValue(self): + """ get most recent value for PV """ + return self.get() + + def putw(self, value, wait=False): + """ set PV value""" + self.put(value, wait=wait) + + def putWait(self, value): + """ put PV value, waits for the callback to + occur before it returns. """ + self.put(value, wait=True) + diff --git a/epics/dbr.py b/epics/dbr.py new file mode 100644 index 0000000..bf82bd6 --- /dev/null +++ b/epics/dbr.py @@ -0,0 +1,385 @@ +#!/usr/bin/env python +# M Newville +# The University of Chicago, 2010 +# Epics Open License +# +# Epics Database Records (DBR) Constants and Definitions +# most of the code here is copied from db_access.h +# +""" constants and declaration of data types for Epics database records +This is mostly copied from CA header files +""" +import ctypes +import functools +import os +import sys +import platform + +HAS_NUMPY = False +try: + import numpy + HAS_NUMPY = True +except ImportError: + pass + +PY64_WINDOWS = (os.name == 'nt' and platform.architecture()[0].startswith('64')) +IRON_PYTHON = platform.python_implementation() == 'IronPython' +PY_MAJOR, PY_MINOR = sys.version_info[:2] + +# EPICS Constants +ECA_NORMAL = 1 +ECA_TIMEOUT = 80 +ECA_IODONE = 339 +ECA_ISATTACHED = 424 +ECA_BADCHID = 410 + +CS_CONN = 2 +OP_CONN_UP = 6 +OP_CONN_DOWN = 7 + +CS_NEVER_SEARCH = 4 +# +# Note that DBR_XXX should be replaced with dbr.XXX +# +STRING = 0 +INT = 1 +SHORT = 1 +FLOAT = 2 +ENUM = 3 +CHAR = 4 +LONG = 5 +DOUBLE = 6 + +TIME_STRING = 14 +TIME_INT = 15 +TIME_SHORT = 15 +TIME_FLOAT = 16 +TIME_ENUM = 17 +TIME_CHAR = 18 +TIME_LONG = 19 +TIME_DOUBLE = 20 + +CTRL_STRING = 28 +CTRL_INT = 29 +CTRL_SHORT = 29 +CTRL_FLOAT = 30 +CTRL_ENUM = 31 +CTRL_CHAR = 32 +CTRL_LONG = 33 +CTRL_DOUBLE = 34 + +MAX_STRING_SIZE = 40 +MAX_UNITS_SIZE = 8 +MAX_ENUM_STRING_SIZE = 26 +MAX_ENUMS = 16 + +#EPICS2UNIX_EPOCH = 631173600.0 - time.timezone +EPICS2UNIX_EPOCH = 631152000.0 + +# create_subscription mask constants +DBE_VALUE = 1 +DBE_LOG = 2 +DBE_ALARM = 4 +DBE_PROPERTY = 8 + +chid_t = ctypes.c_long + +# Note that Windows needs to be told that chid is 8 bytes for 64-bit, +# except that Python2 is very weird -- using a 4byte chid for 64-bit, +# but needing a 1 byte padding! +if PY64_WINDOWS and PY_MAJOR > 2: + chid_t = ctypes.c_int64 + +short_t = ctypes.c_short +ushort_t = ctypes.c_ushort +int_t = ctypes.c_int +uint_t = ctypes.c_uint +long_t = ctypes.c_long +ulong_t = ctypes.c_ulong +float_t = ctypes.c_float +double_t = ctypes.c_double +byte_t = ctypes.c_byte +ubyte_t = ctypes.c_ubyte +string_t = ctypes.c_char * MAX_STRING_SIZE +char_t = ctypes.c_char +char_p = ctypes.c_char_p +void_p = ctypes.c_void_p +py_obj = ctypes.py_object + +value_offset = None + +# extended DBR types: +class TimeStamp(ctypes.Structure): + "emulate epics timestamp" + _fields_ = [('secs', uint_t), ('nsec', uint_t)] + +_STAT_SEV = (('status', short_t), ('severity', short_t)) +_STAT_SEV_TS = (('status', short_t), ('severity', short_t), + ('stamp', TimeStamp)) +_UNITS = ('units', char_t * MAX_UNITS_SIZE) + +def make_unixtime(stamp): + "UNIX timestamp (seconds) from Epics TimeStamp structure" + return (EPICS2UNIX_EPOCH + stamp.secs + 1.e-6*int(1.e-3*stamp.nsec)) + + +class time_string(ctypes.Structure): + "dbr time string" + _fields_ = list(_STAT_SEV_TS) + [('value', MAX_STRING_SIZE*char_t)] + + +class time_short(ctypes.Structure): + "dbr time short" + _fields_ = list(_STAT_SEV_TS) + [('RISC_pad', short_t), + ('value', short_t)] + +class time_float(ctypes.Structure): + "dbr time float" + _fields_ = list(_STAT_SEV_TS) + [('value', float_t)] + +class time_enum(ctypes.Structure): + "dbr time enum" + _fields_ = list(_STAT_SEV_TS) + [('RISC_pad', short_t), + ('value', ushort_t)] + +class time_char(ctypes.Structure): + "dbr time char" + _fields_ = list(_STAT_SEV_TS) + [('RISC_pad0', short_t), + ('RISC_pad1', byte_t), + ('value', byte_t)] + +class time_long(ctypes.Structure): + "dbr time long" + _fields_ = list(_STAT_SEV_TS) + [('value', int_t)] + + +class time_double(ctypes.Structure): + "dbr time double" + _fields_ = list(_STAT_SEV_TS) + [('RISC_pad', int_t), + ('value', double_t)] + +# DBR types with full control and graphical fields +# yes, this strange order is as in db_access.h!!! +ctrl_limits = ('upper_disp_limit', 'lower_disp_limit', + 'upper_alarm_limit', 'upper_warning_limit', + 'lower_warning_limit','lower_alarm_limit', + 'upper_ctrl_limit', 'lower_ctrl_limit') + +def _gen_ctrl_lims(t=short_t): + "create types for control limits" + return [(s, t) for s in ctrl_limits] + +class ctrl_enum(ctypes.Structure): + "dbr ctrl enum" + _fields_ = list(_STAT_SEV) + _fields_.extend([ ('no_str', short_t), + ('strs', (char_t * MAX_ENUM_STRING_SIZE) * MAX_ENUMS), + ('value', ushort_t)]) + +class ctrl_short(ctypes.Structure): + "dbr ctrl short" + _fields_ = list(_STAT_SEV) + [_UNITS] + _gen_ctrl_lims(t=short_t) + _fields_.extend([('value', short_t )]) + +class ctrl_char(ctypes.Structure): + "dbr ctrl long" + _fields_ = list(_STAT_SEV) +[_UNITS] + _gen_ctrl_lims(t=byte_t) + _fields_.extend([('RISC_pad', byte_t), ('value', ubyte_t)]) + +class ctrl_long(ctypes.Structure): + "dbr ctrl long" + _fields_ = list(_STAT_SEV) +[_UNITS] + _gen_ctrl_lims(t=int_t) + _fields_.extend([('value', int_t)]) + +class ctrl_float(ctypes.Structure): + "dbr ctrl float" + _fields_ = list(_STAT_SEV) + _fields_.extend([('precision', short_t), + ('RISC_pad', short_t)] + [_UNITS]) + _fields_.extend( _gen_ctrl_lims(t=float_t) ) + _fields_.extend([('value', float_t)]) + + +class ctrl_double(ctypes.Structure): + "dbr ctrl double" + _fields_ = list(_STAT_SEV) + _fields_.extend([('precision', short_t), + ('RISC_pad', short_t)] + [_UNITS]) + _fields_.extend( _gen_ctrl_lims(t=double_t) ) + _fields_.extend([('value', double_t)]) + + +NP_Map = {} +if HAS_NUMPY: + NP_Map = {INT: numpy.int16, + FLOAT: numpy.float32, + ENUM: numpy.uint16, + CHAR: numpy.uint8, + LONG: numpy.int32, + DOUBLE: numpy.float64} + + +# map of Epics DBR types to ctypes types +Map = {STRING: string_t, + INT: short_t, + FLOAT: float_t, + ENUM: ushort_t, + CHAR: ubyte_t, + LONG: int_t, + DOUBLE: double_t, + + TIME_STRING: time_string, + TIME_INT: time_short, + TIME_SHORT: time_short, + TIME_FLOAT: time_float, + TIME_ENUM: time_enum, + TIME_CHAR: time_char, + TIME_LONG: time_long, + TIME_DOUBLE: time_double, + # Note: there is no ctrl string in the C definition + CTRL_STRING: time_string, + CTRL_SHORT: ctrl_short, + CTRL_INT: ctrl_short, + CTRL_FLOAT: ctrl_float, + CTRL_ENUM: ctrl_enum, + CTRL_CHAR: ctrl_char, + CTRL_LONG: ctrl_long, + CTRL_DOUBLE: ctrl_double + } + +def native_type(ftype): + "return native field type from TIME or CTRL variant" + if ftype == CTRL_STRING: + ftype = TIME_STRING + ntype = ftype + if ftype > CTRL_STRING: + ntype -= CTRL_STRING + elif ftype >= TIME_STRING: + ntype -= TIME_STRING + return ntype + +def Name(ftype, reverse=False): + """ convert integer data type to dbr Name, or optionally reverse that + look up (that is, name to integer)""" + m = {STRING: 'STRING', + INT: 'INT', + FLOAT: 'FLOAT', + ENUM: 'ENUM', + CHAR: 'CHAR', + LONG: 'LONG', + DOUBLE: 'DOUBLE', + + TIME_STRING: 'TIME_STRING', + TIME_SHORT: 'TIME_SHORT', + TIME_FLOAT: 'TIME_FLOAT', + TIME_ENUM: 'TIME_ENUM', + TIME_CHAR: 'TIME_CHAR', + TIME_LONG: 'TIME_LONG', + TIME_DOUBLE: 'TIME_DOUBLE', + + CTRL_STRING: 'CTRL_STRING', + CTRL_SHORT: 'CTRL_SHORT', + CTRL_FLOAT: 'CTRL_FLOAT', + CTRL_ENUM: 'CTRL_ENUM', + CTRL_CHAR: 'CTRL_CHAR', + CTRL_LONG: 'CTRL_LONG', + CTRL_DOUBLE: 'CTRL_DOUBLE', + } + if reverse: + name = ftype.upper() + if name in list(m.values()): + for key, val in m.items(): + if name == val: return key + return m.get(ftype, 'unknown') + +def cast_args(args): + """returns casted array contents + + returns: [dbr_ctrl or dbr_time struct, + count * native_type structs] + + If data is already of a native_type, the first + value in the list will be None. + """ + ftype = args.type + if ftype not in Map: + ftype = double_t + + ntype = native_type(ftype) + if ftype != ntype: + native_start = args.raw_dbr + value_offset[ftype] + return [ctypes.cast(args.raw_dbr, + ctypes.POINTER(Map[ftype])).contents, + ctypes.cast(native_start, + ctypes.POINTER(args.count * Map[ntype])).contents + ] + else: + return [None, + ctypes.cast(args.raw_dbr, + ctypes.POINTER(args.count * Map[ftype])).contents + ] + + +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): + "event handler arguments" + _fields_ = [('usr', ctypes.py_object), + ('chid', chid_t), + ('type', long_t), + ('count', long_t), + ('raw_dbr', void_p), + ('status', int_t)] + +class connection_args(ctypes.Structure): + "connection arguments" + _fields_ = [('chid', chid_t), + ('op', long_t)] + + +class access_rights_handler_args(ctypes.Structure): + "access rights arguments" + _fields_ = [('chid', chid_t), + ('access', ubyte_t)] + +if PY64_WINDOWS and PY_MAJOR == 2: + # need to add padding on 64-bit Windows for Python2 -- yuck! + class event_handler_args(ctypes.Structure): + "event handler arguments" + _fields_ = [('usr', ctypes.py_object), + ('chid', chid_t), + ('_pad_', ctypes.c_int8), + ('type', ctypes.c_int32), + ('count', ctypes.c_int32), + ('raw_dbr', void_p), + ('status', ctypes.c_int32)] + + class connection_args(ctypes.Structure): + "connection arguments" + _fields_ = [('chid', chid_t), + ('_pad_',ctypes.c_int8), + ('op', long_t)] + + + class access_rights_handler_args(ctypes.Structure): + "access rights arguments" + _fields_ = [('chid', chid_t), + ('_pad_',ctypes.c_int8), + ('access', ubyte_t)] diff --git a/epics/device.py b/epics/device.py new file mode 100644 index 0000000..dde8e70 --- /dev/null +++ b/epics/device.py @@ -0,0 +1,326 @@ +#!/usr/bin/env python +# M Newville +# The University of Chicago, 2010 +# Epics Open License +""" +basic device object defined +""" +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'. + + Many groups of PVs will have names made up of + Prefix+Delimiter+Attribute + with common Prefix and Delimiter, but a range of Attribute names. + Many Epics Records follow this model, but a Device is only about + PV names, and so is not exactly a mapping to an Epics Record. + + This class allows a collection of PVs to be represented simply. + + >>> dev = epics.Device('XX:m1', delim='.') + >>> dev.put('OFF',0 ) + >>> dev.put('VAL', 0.25) + >>> dev.get('RBV') + >>> print dev.FOFF + >>> print dev.get('FOFF', as_string=True) + + This will put a 0 to XX:m1.OFF, a 0.25 to XX:m1.VAL, and then + get XX:m1.RBV and XX.m1.FOFF. + + Note access to the underlying PVs can either be with get()/put() + methods or with attributes derived from the Attribute (Suffix) for + that PV. Thus + >>> print dev.FOFF + >>> print dev.get('FOFF') + + are equivalent, as are: + >>> dev.VAL = 1 + >>> dev.put('VAL', 1) + + The methods do provide more options. For example, to get the PV + value as a string, + Device.get(Attribute, as_string=True) + must be used. To put-with-wait, use + Device.put(Attribute, value, wait=True) + + The list of attributes can be pre-loaded at initialization time. + + + The attribute PVs are built as needed and held in an internal + buffer (self._pvs). This class is kept intentionally simple + so that it may be subclassed. + + To pre-load attribute names on initialization, provide a + list or tuple of attributes: + + >>> struck = epics.Device('13IDC:str:', + ... attrs=('ChannelAdvance', + ... 'EraseStart','StopAll')) + >>> print struck.PV('ChannelAdvance').char_value + 'External' + + The prefix is optional, and when left off, this class can + be used as an arbitrary container of PVs, or to turn + any subclass into an epics Device: + + >>> class MyClass(epics.Device): + ... def __init__(self,**kw): + ... epics.Device.__init__() # no Prefix!! + ... + >>> x = MyClass() + >>> pv_m1 = x.PV('13IDC:m1.VAL') + >>> x.put('13IDC:m3.VAL', 2) + >>> print x.PV('13IDC:m3.DIR').get(as_string=True) + + Attribute aliases can also be used to be make the device + more user-friendly: + + >>> dev = epics.Device('IOC:m1', delim='.', + ... aliases={'readback': 'RBV'}) + >>> print 'rbv is', dev.RBV # IOC:m1.RBV + >>> print 'readback is', dev.readback # also IOC:m1.RBV + + If you encounter issues with introspection (with IPython, + for example), and your device has a well-defined list of + attributes, consider clearing the `mutable` flag. Attributes + not already in the list will then be assumed to be invalid. + AttributeError is raised quickly without using channel access: + + >>> dev = epics.Device('IOC:m1', delim='.', mutable=False, + ... aliases={'readback': 'RBV'}) + >>> print dev.foobar + Traceback (most recent call last): + ... + AttributeError: Device has no attribute foobar + """ + + _prefix = None + _delim = '' + _pvs = {} + _init = False + _aliases = {} + _mutable = True + _nonpvs = ('_prefix', '_pvs', '_delim', '_init', '_aliases', + '_mutable', '_nonpvs') + def __init__(self, prefix='', attrs=None, + nonpvs=None, delim='', timeout=None, + mutable=True, aliases=None, with_poll=True): + + self._nonpvs = list(self._nonpvs) + self._delim = delim + self._prefix = prefix + delim + self._pvs = {} + self._mutable = mutable + if aliases is None: + aliases = {} + self._aliases = aliases + if nonpvs is not None: + for npv in nonpvs: + if npv not in self._nonpvs: + self._nonpvs.append(npv) + + if attrs is not None: + for attr in attrs: + 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, timeout=timeout) + + if with_poll: + poll() + self._init = True + + def PV(self, attr, connect=True, **kw): + """return epics.PV for a device attribute""" + if attr in self._aliases: + attr = self._aliases[attr] + + if attr not in self._pvs: + pvname = attr + if self._prefix is not None: + pvname = "%s%s" % (self._prefix, attr) + self._pvs[attr] = get_pv(pvname, **kw) + if connect and not self._pvs[attr].connected: + self._pvs[attr].wait_for_connection() + return self._pvs[attr] + + def add_pv(self, pvname, attr=None, **kw): + """add a PV with an optional attribute name that may not exactly + correspond to the mapping of Attribute -> Prefix + Delim + Attribute + That is, with a device defined as + >>> dev = Device('XXX', delim='.') + + getting the VAL attribute + >>> dev.get('VAL') # or dev.VAL + + becomes 'caget(XXX.VAL)'. With add_pv(), one can add a + non-conforming PV to the collection: + >>> dev.add_pv('XXX_status.VAL', attr='status') + + and then use as + >>> dev.get('status') # or dev.status + + If attr is not specified, the full pvname will be used. + """ + if attr is None: + attr = pvname + self._pvs[attr] = get_pv(pvname, **kw) + return self._pvs[attr] + + def put(self, attr, value, wait=False, use_complete=False, timeout=10): + """put an attribute value, + optionally wait for completion or + up to a supplied timeout value""" + 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 an attribute value, + option as_string returns a string representation""" + 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 + current attributes""" + out = {} + for key in self._pvs: + out[key] = self._pvs[key].get() + if (self._pvs[key].count > 1 and + 'char' == self._pvs[key].type): + out[key] = self._pvs[key].get(as_string=True) + return out + + def restore_state(self, state): + """restore a dictionary of the values, as saved from save_state""" + for key, val in state.items(): + if key in self._pvs and 'write' in self._pvs[key].access: + self._pvs[key].put(val) + + def write_state(self, fname, state=None): + """write save state to external file. + If state is not provided, the current state is used + + Note that this only writes data for PVs with write-access, and count=1 (except CHAR """ + if state is None: + state = self.save_state() + out = ["#Device Saved State for %s, prefx='%s': %s\n" % (self.__class__.__name__, + self._prefix, time.ctime())] + for key in sorted(state.keys()): + if (key in self._pvs and + 'write' in self._pvs[key].access and + (1 == self._pvs[key].count or + 'char' == self._pvs[key].type)): + out.append("%s %s\n" % (key, state[key])) + fout = open(fname, 'w') + fout.writelines(out) + fout.close() + + + def read_state(self, fname, restore=False): + """read state from file, optionally restore it""" + finp = open(fname, 'r') + textlines = finp.readlines() + finp.close() + state = {} + for line in textlines: + if line.startswith('#'): + continue + key, strval = line[:-1].split(' ', 1) + if key in self._pvs: + dtype = self._pvs[key].type + count = self._pvs[key].count + val = strval + if dtype in ('double', 'float'): + val = float(val) + elif dtype in ('int', 'long', 'short', 'enum'): + val = int(val) + state[key] = val + if restore: + self.restore_state(state) + return state + + + def get_all(self): + """return a dictionary of the values of all + current attributes""" + return self.save_state() + + def add_callback(self, attr, callback, **kws): + """add a callback function to an attribute PV, + so that the callback function will be run when + the attribute's value changes""" + self.PV(attr).get() + return self.PV(attr).add_callback(callback, **kws) + + def remove_callbacks(self, attr, index=None): + """remove a callback function to an attribute PV""" + self.PV(attr).remove_callback(index=index) + + + def __getattr__(self, attr): + if attr in self._aliases: + attr = self._aliases[attr] + + if attr in self._pvs: + return self.get(attr) + elif attr in self.__dict__: + return self.__dict__[attr] + elif self._init and self._mutable and not attr.startswith('__'): + pv = self.PV(attr, connect=True) + if pv.connected: + return pv.get() + + raise AttributeError('%s has no attribute %s' % (self.__class__.__name__, + attr)) + + def __setattr__(self, attr, val): + if attr in self._aliases: + attr = self._aliases[attr] + + if attr in self._nonpvs: + self.__dict__[attr] = val + elif attr in self._pvs: + self.put(attr, val) + elif self._init and self._mutable and not attr.startswith('__'): + try: + self.PV(attr) + self.put(attr, val) + except: + raise AttributeError('%s has no attribute %s' % (self.__class__.__name__, + attr)) + elif attr in self.__dict__: + self.__dict__[attr] = val + elif self._init: + raise AttributeError('%s has no attribute %s' % (self.__class__.__name__, + attr)) + + def __dir__(self): + # there's no cleaner method to do this until Python 3.3 + 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): + "string representation" + pref = self._prefix + if pref.endswith('.'): + pref = pref[:-1] + return "" % (pref, len(self._pvs)) + + + def pv_property(attr, as_string=False, wait=False, timeout=10.0): + return property(lambda self: \ + self.get(attr, as_string=as_string), + lambda self,val: \ + self.put(attr, val, wait=wait, timeout=timeout), + None, None) diff --git a/epics/devices/__init__.py b/epics/devices/__init__.py new file mode 100644 index 0000000..e8a989e --- /dev/null +++ b/epics/devices/__init__.py @@ -0,0 +1,23 @@ +""" +simple devices +""" +from .ai import ai +from .ao import ao +from .bi import bi +from .bo import bo + +from .scaler import Scaler +from .struck import Struck +from .srs570 import SRS570 +from .mca import DXP, MCA, MultiXMAP, ROI +from .scan import Scan +from .transform import Transform + +from .ad_base import AD_Camera +from .ad_fileplugin import AD_FilePlugin +from .ad_image import AD_ImagePlugin +from .ad_overlay import AD_OverlayPlugin +from .ad_perkinelmer import AD_PerkinElmer + +Mca = MCA + diff --git a/epics/devices/ad_base.py b/epics/devices/ad_base.py new file mode 100644 index 0000000..c20a7c3 --- /dev/null +++ b/epics/devices/ad_base.py @@ -0,0 +1,40 @@ +from .. import Device + +class AD_Camera(Device): + """ + Basic AreaDetector Camera Device + """ + attrs = ("Acquire", "AcquirePeriod", "AcquirePeriod_RBV", + "AcquireTime", "AcquireTime_RBV", + "ArrayCallbacks", "ArrayCallbacks_RBV", + "ArrayCounter", "ArrayCounter_RBV", "ArrayRate_RBV", + "ArraySizeX_RBV", "ArraySizeY_RBV", "ArraySize_RBV", + "BinX", "BinX_RBV", "BinY", "BinY_RBV", + "ColorMode", "ColorMode_RBV", + "DataType", "DataType_RBV", "DetectorState_RBV", + "Gain", "Gain_RBV", "ImageMode", "ImageMode_RBV", + "MaxSizeX_RBV", "MaxSizeY_RBV", + "MinX", "MinX_RBV", "MinY", "MinY_RBV", + "NumImages", "NumImagesCounter_RBV", "NumImages_RBV", + "SizeX", "SizeX_RBV", "SizeY", "SizeY_RBV", + "TimeRemaining_RBV", + "TriggerMode", "TriggerMode_RBV", "TriggerSoftware") + + + _nonpvs = ('_prefix', '_pvs', '_delim') + + def __init__(self, prefix): + Device.__init__(self, prefix, delim='', mutable=False, + attrs=self.attrs) + + def ensure_value(self, attr, value, wait=False): + """ensures that an attribute with an associated _RBV value is + set to the specifed value + """ + rbv_attr = "%s_RBV" % attr + if rbv_attr not in self._pvs: + return self._pvs[attr].put(value, wait=wait) + + if self._pvs[rbv_attr].get(as_string=True) != value: + self._pvs[attr].put(value, wait=wait) + diff --git a/epics/devices/ad_fileplugin.py b/epics/devices/ad_fileplugin.py new file mode 100644 index 0000000..e8e5c3c --- /dev/null +++ b/epics/devices/ad_fileplugin.py @@ -0,0 +1,94 @@ +from .. import Device + +class AD_FilePlugin(Device): + """ + AreaDetector File Plugin + """ + attrs = ("AutoIncrement", "AutoIncrement_RBV", + "AutoSave", "AutoSave_RBV", + "Capture", "Capture_RBV", + "EnableCallbacks", "EnableCallbacks_RBV", + "FileName", "FileName_RBV", + "FileNumber", "FileNumber_RBV", + "FilePath", "FilePath_RBV", + "FilePathExists_RBV", + "FileTemplate", "FileTemplate_RBV", + "FileWriteMode", "FileWriteMode_RBV", + "FullFileName_RBV", + "NDArrayPort", "NDArrayPort_RBV", + "NumCapture", "NumCapture_RBV", "NumCaptured_RBV", + "ReadFile", "ReadFile_RBV", + "WriteFile", "WriteFile_RBV", + "WriteMessage", "WriteStatus") + + _nonpvs = ('_prefix', '_pvs', '_delim') + + def __init__(self, prefix): + Device.__init__(self, prefix, delim='', mutable=False, + attrs=self.attrs) + + def ensure_value(self, attr, value, wait=False): + """ensures that an attribute with an associated _RBV value is + set to the specifed value + """ + rbv_attr = "%s_RBV" % attr + if rbv_attr not in self._pvs: + return self._pvs[attr].put(value, wait=wait) + + if self._pvs[rbv_attr].get(as_string=True) != value: + self._pvs[attr].put(value, wait=wait) + + + def setFileName(self,fname): + return self.put('FileName',fname) + + def nextFileNumber(self): + self.setFileNumber(1+self.get('FileNumber')) + + def setFileNumber(self, fnum=None): + if fnum is None: + self.put('AutoIncrement', 1) + else: + self.put('AutoIncrement', 0) + return self.put('FileNumber',fnum) + + def setPath(self,pathname): + return self.put('FilePath', pathname) + + def setTemplate(self, fmt): + return self.put('FileTemplate', fmt) + + def setWriteMode(self, mode): + return self.put('FileWriteMode', mode) + + def getLastFileName(self): + return self.get('FullFileName_RBV',as_string=True) + + def CaptureOn(self): + return self.put('Capture', 1) + + def CaptureOff(self): + return self.put('Capture', 0) + + def setNumCapture(self,n): + return self.put('NumCapture', n) + + def WriteComplete(self): + return (0==self.get('WriteFile_RBV') ) + + def getTemplate(self): + return self.get('FileTemplate_RBV',as_string=True) + + def getName(self): + return self.get('FileName_RBV',as_string=True) + + def getNumber(self): + return self.get('FileNumber_RBV') + + def getPath(self): + return self.get('FilePath_RBV',as_string=True) + + def getFileNameByIndex(self,index): + return self.getTemplate() % (self.getPath(), self.getName(), index) + + diff --git a/epics/devices/ad_image.py b/epics/devices/ad_image.py new file mode 100644 index 0000000..d168eea --- /dev/null +++ b/epics/devices/ad_image.py @@ -0,0 +1,31 @@ +from .. import Device + +class AD_ImagePlugin(Device): + """ + AreaDetector Image Plugin + """ + attrs = ('ArrayData', + 'UniqueId', 'UniqueId_RBV', + 'NDimensions', 'NDimensions_RBV', + 'ArraySize0', 'ArraySize0_RBV', + 'ArraySize1', 'ArraySize1_RBV', + 'ArraySize2', 'ArraySize2_RBV', + 'ColorMode', 'ColorMode_RBV') + + _nonpvs = ('_prefix', '_pvs', '_delim') + + def __init__(self, prefix): + Device.__init__(self, prefix, delim='', mutable=False, + attrs=self.attrs) + + def ensure_value(self, attr, value, wait=False): + """ensures that an attribute with an associated _RBV value is + set to the specifed value + """ + rbv_attr = "%s_RBV" % attr + if rbv_attr not in self._pvs: + return self._pvs[attr].put(value, wait=wait) + + if self._pvs[rbv_attr].get(as_string=True) != value: + self._pvs[attr].put(value, wait=wait) + diff --git a/epics/devices/ad_mca.py b/epics/devices/ad_mca.py new file mode 100644 index 0000000..9e3bcfd --- /dev/null +++ b/epics/devices/ad_mca.py @@ -0,0 +1,293 @@ +import numpy as np +import time + +from epics import PV, caget, caput, poll, Device, get_pv + +MAX_CHAN = 4096 +MAX_ROIS = 32 +TOOMANY_ROIS = 'Too many ROIS, only %i ROIS allowed.' % (MAX_ROIS) + +class ADMCAROI(Device): + """ + MCA ROI using ROIStat plugin from areaDetector2, + as used for Xspress3 detector. + """ + + _attrs =('Use', 'Name', 'MinX', 'SizeX', + 'BgdWidth', 'SizeX_RBV', 'MinX_RBV', + 'Total_RBV', 'Net_RBV') + + _aliases = {'left': 'MinX', + 'width': 'SizeX', + 'name': 'Name', + 'sum': 'Total_RBV', + 'net': 'Net_RBV'} + + _nonpvs = ('_prefix', '_pvs', '_delim', '_init', + '_aliases', 'data_pv') + + _reprfmt = "" + def __init__(self, prefix, roi=1, bgr_width=3, data_pv=None, with_poll=False): + self._prefix = '%s:%i' % (prefix, roi) + Device.__init__(self, self._prefix, delim=':', + attrs=('Name', 'MinX'), with_poll=with_poll) + self._aliases = {'left': 'MinX', + 'width': 'SizeX', + 'name': 'Name', + 'sum': 'Total_RBV', + 'net': 'Net_RBV'} + + self.data_pv = data_pv + + def __eq__(self, other): + """used for comparisons""" + return (self.MinX == getattr(other, 'MinX', None) and + self.SizeX == getattr(other, 'SizeX', None) and + self.BgdWidth == getattr(other, 'BgdWidth', None) ) + + def __ne__(self, other): return not self.__eq__(other) + def __lt__(self, other): return self.MinX < getattr(other, 'MinX', None) + def __le__(self, other): return self.MinX <= getattr(other, 'MinX', None) + def __gt__(self, other): return self.MinX > getattr(other, 'MinX', None) + def __ge__(self, other): return self.MinX >= getattr(other, 'MinX', None) + + def __repr__(self): + "string representation" + pref = self._prefix + if pref.endswith('.'): + pref = pref[:-1] + + return self._reprfmt % (pref, self.Name, self.MinX, + self.MinX+self.SizeX) + + def get_right(self): + return self.MinX + self.SizeX + + def set_right(self, val): + """set the upper ROI limit (adjusting size, leaving left unchanged)""" + self._pvs['SizeX'].put(val - self.MinX) + + right = property(get_right, set_right) + + def get_center(self): + return int(round(self.MinX + self.SizeX/2.0)) + + def set_center(self, val): + """set the ROI center (adjusting left, leaving width unchanged)""" + self._pvs['MinX'].put(int(round(val - self.SizeX/2.0))) + + center = property(get_center, set_center) + + def clear(self): + self.Name = '' + self.MinX = 0 + self.SizeX = 0 + + def get_counts(self, data=None, net=False): + """ + calculate total and net counts for a spectra + + Parameters: + ----------- + data numpy array of spectra or None to read from PV + net bool to set net counts (default=False: total counts returned) + """ + if data is None and self.data_pv is not None: + data = self.data_pv.get() + out = self.Total_RBV + if net: + out = self.Net_RBV + if isinstance(data, np.ndarray): + lo = self.MinX + hi = self.MinX + self.SizeX + out = data[lo:hi+1].sum() + if net: + wid = int(self.bgr_width) + jlo = max((lo - wid), 0) + jhi = min((hi + wid), len(data)-1) + 1 + bgr = np.concatenate((data[jlo:lo], + data[hi+1:jhi])).mean() + out = out - bgr*(hi-lo) + return out + +class ADMCA(Device): + """ + MCA using ROIStat plugin from areaDetector2, + as used for Xspress3 detector. + """ + _attrs =('AcquireTime', 'Acquire', 'NumImages') + _nonpvs = ('_prefix', '_pvs', '_delim', '_roi_prefix', + '_npts', 'rois', '_nrois', 'rois', '_calib') + _calib = (0.00, 0.01, 0.00) + def __init__(self, prefix, data_pv=None, nrois=None, roi_prefix=None): + + self._prefix = prefix + Device.__init__(self, self._prefix, delim='', + attrs=self._attrs, with_poll=False) + if data_pv is not None: + self._pvs['VAL'] = PV(data_pv) + self._npts = None + self._nrois = nrois + if self._nrois is None: + self._nrois = MAX_ROIS + + self._roi_prefix = roi_prefix + for i in range(self._nrois): + p = get_pv('%s:%i:Name' % (self._roi_prefix, i+1)) + p = get_pv('%s:%i:MinX' % (self._roi_prefix, i+1)) + p = get_pv('%s:%i:SizeX' % (self._roi_prefix, i+1)) + self.get_rois() + poll() + + def start(self): + "Start AD MCA" + self.Acquire = 1 + poll() + return self.Acquire + + def stop(self): + "Stop AD MCA" + self.Acquire = 0 + return self.Acquire + + def get_calib(self): + """get energy calibration tuple (offset, slope, quad)""" + return self._calib + + def get_energy(self): + """return energy for AD MCA""" + if self._npts is None and self._pvs['VAL'] is not None: + self._npts = len(self.get('VAL')) + en = np.arange(self._npts, dtype='f8') + cal = self._calib + return cal[0] + en*(cal[1] + en*cal[2]) + + def clear_rois(self, nrois=None): + "clear all rois" + if self.rois is None: + self.get_rois() + for roi in self.rois: + roi.clear() + self.rois = [] + + def get_rois(self, nrois=None): + "get all rois" + self.rois = [] + data_pv = self._pvs['VAL'] + poll() + data_pv.connect() + prefix = self._roi_prefix + if prefix is None: + return self.rois + + if nrois is None: + nrois = self._nrois + for i in range(nrois): + roi = ADMCAROI(prefix=self._roi_prefix, roi=i+1, data_pv=data_pv) + if roi.Name is None: + roi = ADMCAROI(prefix=self._roi_prefix, roi=i+1, + data_pv=data_pv, with_poll=True) + if roi.Name is None: + continue + if len(roi.Name.strip()) > 0 and roi.MinX > 0 and roi.SizeX > 0: + self.rois.append(roi) + else: + break + poll(0.001, 1.0) + return self.rois + + def del_roi(self, roiname): + "delete an roi by name" + if self.rois is None: + self.get_rois() + for roi in self.rois: + if roi.Name.strip().lower() == roiname.strip().lower(): + roi.clear() + poll(0.010, 1.0) + self.sort_rois() + + def add_roi(self, roiname, lo, wid=None, hi=None, sort=True): + """ + add an roi, given name, lo, and hi channels. + """ + if lo is None or (hi is None and wid is None): + return + if self.rois is None: + self.get_rois() + + try: + iroi = len(self.rois) + 1 + except: + iroi = 0 + + if iroi > MAX_ROIS: + raise ValueError(TOOMANY_ROIS) + data_pv = self._pvs['VAL'] + prefix = self._roi_prefix + + roi = ADMCAROI(prefix=prefix, roi=iroi, data_pv=data_pv) + roi.Name = roiname.strip() + + nmax = MAX_CHAN + if self._npts is None and self._pvs['VAL'] is not None: + nmax = self._npts = len(self.get('VAL')) + + roi.MinX = min(nmax-1, lo) + if hi is not None: + hi = min(nmax, hi) + roi.SizeX = hi-lo + elif wid is not None: + roi.SizeX = min(nmax, wid+roi.MinX) - roi.MinX + self.rois.append(roi) + if sort: + self.sort_rois() + + def sort_rois(self): + """ + make sure rois are sorted, and Epics PVs are cleared + """ + if self.rois is None: + self.get_rois() + + poll(0.05, 1.0) + unsorted = [] + empties = 0 + for roi in self.rois: + if len(roi.Name) > 0 and roi.right > 0: + unsorted.append(roi) + else: + empties =+ 1 + if empties > 3: + break + + self.rois = sorted(unsorted) + rpref = self._roi_prefix + roidat = [(r.Name, r.MinX, r.SizeX) for r in self.rois] + + for iroi, roi in enumerate(roidat): + caput("%s:%i:Name" % (rpref, iroi+1), roi[0]) + caput("%s:%i:MinX" % (rpref, iroi+1), roi[1]) + caput("%s:%i:SizeX" % (rpref, iroi+1), roi[2]) + + iroi = len(roidat) + caput("%s:%i:Name" % (rpref, iroi+1), '') + caput("%s:%i:MinX" % (rpref, iroi+1), 0) + caput("%s:%i:SizeX" % (rpref, iroi+1), 0) + self.get_rois() + + def set_rois(self, roidata): + """ + set all rois from list/tuple of (Name, Lo, Hi), + and ensures they are ordered and contiguous. + """ + data_pv = self._pvs['VAL'] + + iroi = 0 + self.clear_rois() + for name, lo, hi in roidata: + if len(name) > 0 and hi > lo and hi > 0: + iroi +=1 + if iroi >= MAX_ROIS: + raise ValueError(TOOMANY_ROIS) + self.add_roi(name, lo, hi=hi, sort=False) + self.sort_rois() diff --git a/epics/devices/ad_overlay.py b/epics/devices/ad_overlay.py new file mode 100644 index 0000000..a6c77b7 --- /dev/null +++ b/epics/devices/ad_overlay.py @@ -0,0 +1,37 @@ +from .. import Device + +class AD_OverlayPlugin(Device): + """ + AreaDetector Overlay Plugin + """ + attrs = ('Name', 'Name_RBV', + 'Use', 'Use_RBV', + 'PositionX', 'PositionX_RBV', + 'PositionY', 'PositionY_RBV', + 'PositionXLink', 'PositionYLink', + 'SizeXLink', 'SizeYLink', + 'SizeX', 'SizeX_RBV', + 'SizeY', 'SizeY_RBV', + 'Shape', 'Shape_RBV', + 'DrawMode', 'DrawMode_RBV', + 'Red', 'Red_RBV', + 'Green', 'Green_RBV', + 'Blue', 'Blue_RBV') + + _nonpvs = ('_prefix', '_pvs', '_delim') + + def __init__(self, prefix): + Device.__init__(self, prefix, delim='', mutable=False, + attrs=self.attrs) + + def ensure_value(self, attr, value, wait=False): + """ensures that an attribute with an associated _RBV value is + set to the specifed value + """ + rbv_attr = "%s_RBV" % attr + if rbv_attr not in self._pvs: + return self._pvs[attr].put(value, wait=wait) + + if self._pvs[rbv_attr].get(as_string=True) != value: + self._pvs[attr].put(value, wait=wait) + diff --git a/epics/devices/ad_perkinelmer.py b/epics/devices/ad_perkinelmer.py new file mode 100644 index 0000000..a5780a8 --- /dev/null +++ b/epics/devices/ad_perkinelmer.py @@ -0,0 +1,189 @@ +#!/usr/bin/python +import sys +import time +from .. import Device + +class AD_PerkinElmer(Device): + camattrs = ('PEAcquireOffset', 'PENumOffsetFrames', + 'ImageMode', 'TriggerMode', + 'Acquire', 'AcquireTime', 'Model_RBV', + 'NumImages', 'ShutterControl', 'ShutterMode') + + pathattrs = ('FilePath', 'FileTemplate', 'FileWriteMode', + 'FileName', 'FileNumber', 'FullFileName_RBV', + 'Capture', 'Capture_RBV', 'NumCapture', 'WriteFile_RBV', + 'AutoSave', 'EnableCallbacks', 'ArraySize0_RBV', + 'FileTemplate_RBV', 'FileName_RBV', 'AutoIncrement') + + _nonpvs = ('_prefix', '_pvs', '_delim', 'filesaver', + 'camattrs', 'pathattrs', '_nonpvs') + + def __init__(self,prefix, filesaver='netCDF1:'): + camprefix = prefix + 'cam1:' + Device.__init__(self, camprefix, delim='', + mutable=False, + attrs=self.camattrs) + self.filesaver = "%s%s" % (prefix, filesaver) + for p in self.pathattrs: + pvname = '%s%s%s' % (prefix, filesaver, p) + self.add_pv(pvname, attr='File_'+p) + + + def AcquireOffset(self, timeout=10, open_shutter=True): + """Acquire Offset -- a slightly complex process + + Arguments + --------- + timeout : float (default 10) time in seconds to wait + open_shutter : bool (default True) open shutters on exit + + 1. close shutter + 2. set image mode to single /internal trigger + 3. acquire offset correction + 4. reset image mode and trigger mode + 5. optionally (by default) open shutter + """ + self.ShutterMode = 1 + self.ShutterControl = 0 + image_mode_save = self.ImageMode + trigger_mode_save = self.TriggerMode + self.ImageMode = 0 + self.TriggerMode = 0 + offtime = self.PENumOffsetFrames * self.AcquireTime + time.sleep(0.50) + self.PEAcquireOffset = 1 + t0 = time.time() + time.sleep(offtime/3.0) + while self.PEAcquireOffset > 0 and time.time()-t0 < timeout+offtime: + time.sleep(0.1) + time.sleep(1.00) + self.ImageMode = image_mode_save + self.TriggerMode = trigger_mode_save + time.sleep(1.00) + if open_shutter: + self.ShutterControl = 1 + self.ShutterMode = 0 + time.sleep(1.00) + + def SetExposureTime(self, t, open_shutter=True): + "set exposure time, re-acquire offset correction" + self.AcquireTime = t + self.AcquireOffset(open_shutter=open_shutter) + + def SetMultiFrames(self, n, trigger='external'): + """set number of multiple frames for streaming + this sets number of images for camera in Multiple Image Mode + AND sets the number of images to capture with file plugin + """ + self.ImageMode = 1 # multiple images + + # trigger mode + trigger_mode = 0 # internal + if trigger.lower().startswith('ext'): + trigger_mode = 1 # external + elif trigger.lower().startswith('free'): + trigger_mode = 2 # free running + elif trigger.lower().startswith('soft'): + trigger_mode = 3 # soft trigger + time.sleep(0.1) + + self.TriggerMode = trigger_mode + # number of images for collection and streaming + self.NumImages = n + # set filesaver + self.filePut('NumCapture', n) + self.filePut('EnableCallbacks', 1) + self.filePut('FileNumber', 1) + self.filePut('AutoIncrement', 1) + time.sleep(2.0) + + def StartStreaming(self): + """start streamed acquisition to save with + file saving plugin, and start acquisition + """ + self.ShutterMode = 0 + self.filePut('AutoSave', 1) + self.filePut('FileWriteMode', 2) # stream + time.sleep(0.05) + self.filePut('Capture', 1) # stream + self.Acquire = 1 + time.sleep(0.25) + + + def FinishStreaming(self, timeout=5.0): + """start streamed acquisition to save with + file saving plugin, and start acquisition + """ + t0 = time.time() + capture_on = self.fileGet('Capture_RBV') + while capture_on==1 and time.time() - t0 < timeout: + time.sleep(0.05) + capture_on = self.fileGet('Capture_RBV') + if capture_on != 0: + print( 'Forcing XRD Streaming to stop') + self.filePut('Capture', 0) + t0 = time.time() + while capture_on==1 and time.time() - t0 < timeout: + time.sleep(0.05) + capture_on = self.fileGet('Capture_RBV') + time.sleep(0.50) + + + def filePut(self, attr, value, **kw): + return self.put("File_%s" % attr, value, **kw) + + def fileGet(self, attr, **kw): + return self.get("File_%s" % attr, **kw) + + def setFilePath(self, pathname): + return self.filePut('FilePath', pathname) + + def setFileTemplate(self, fmt): + return self.filePut('FileTemplate', fmt) + + def setFileWriteMode(self, mode): + return self.filePut('FileWriteMode', mode) + + def setFileName(self, fname): + return self.filePut('FileName', fname) + + def nextFileNumber(self): + self.setFileNumber(1+self.fileGet('FileNumber')) + + def setFileNumber(self, fnum=None): + if fnum is None: + self.filePut('AutoIncrement', 1) + else: + self.filePut('AutoIncrement', 0) + return self.filePut('FileNumber',fnum) + + def getLastFileName(self): + return self.fileGet('FullFileName_RBV',as_string=True) + + def FileCaptureOn(self): + return self.filePut('Capture', 1) + + def FileCaptureOff(self): + return self.filePut('Capture', 0) + + def setFileNumCapture(self,n): + return self.filePut('NumCapture', n) + + def FileWriteComplete(self): + return (0==self.fileGet('WriteFile_RBV') ) + + def getFileTemplate(self): + return self.fileGet('FileTemplate_RBV',as_string=True) + + def getFileName(self): + return self.fileGet('FileName_RBV',as_string=True) + + def getFileNumber(self): + return self.fileGet('FileNumber_RBV') + + def getFilePath(self): + return self.fileGet('FilePath_RBV',as_string=True) + + def getFileNameByIndex(self,index): + return self.getFileTemplate() % (self.getFilePath(), self.getFileName(), index) + diff --git a/epics/devices/ai.py b/epics/devices/ai.py new file mode 100644 index 0000000..3697c1b --- /dev/null +++ b/epics/devices/ai.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python +"""Epics analog input record""" +from .. import Device + +class ai(Device): + "Simple analog input device" + + attrs = ('VAL', 'EGU', 'HOPR', 'LOPR', 'PREC', 'NAME', 'DESC', + 'DTYP', 'INP', 'LINR', 'RVAL', 'ROFF', 'EGUF', 'EGUL', + 'AOFF', 'ASLO', 'ESLO', 'EOFF', 'SMOO', 'HIHI', 'LOLO', + 'HIGH', 'LOW', 'HHSV', 'LLSV', 'HSV', 'LSV', 'HYST') + + def __init__(self, prefix, **kwargs): + if prefix.endswith('.'): + prefix = prefix[:-1] + Device.__init__(self, prefix, delim='.', attrs=self.attrs, **kwargs) diff --git a/epics/devices/ao.py b/epics/devices/ao.py new file mode 100644 index 0000000..f64e315 --- /dev/null +++ b/epics/devices/ao.py @@ -0,0 +1,16 @@ +#!/usr/bin/ao python +from .. import Device + +class ao(Device): + "Simple analog output device" + + attrs = ('OUT', 'LINR', 'RVAL', 'ROFF', 'EGUF', 'EGUL', 'AOFF', + 'ASLO', 'ESLO', 'EOFF', 'VAL', 'EGU', 'HOPR', 'LOPR', + 'PREC', 'NAME', 'DESC', 'DTYP', 'HIHI', 'LOLO', 'HIGH', + 'LOW', 'HHSV', 'LLSV', 'HSV', 'LSV', 'HYST', 'OMSL', 'DOL', + 'OIF', 'DRVH', 'DRVL', 'OROC', 'OVAL') + + def __init__(self, prefix, **kwargs): + if prefix.endswith('.'): + prefix = prefix[:-1] + Device.__init__(self, prefix, delim='.', attrs=self.attrs, **kwargs) diff --git a/epics/devices/bi.py b/epics/devices/bi.py new file mode 100644 index 0000000..27571bc --- /dev/null +++ b/epics/devices/bi.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python +from .. import Device + +class bi(Device): + """ + Simple binary input device + """ + + attrs = ('INP', 'ZNAM', 'ONAM', 'RVAL', 'VAL', 'EGU', 'HOPR', 'LOPR', + 'PREC', 'NAME', 'DESC', 'DTYP') + + def __init__(self, prefix, **kwargs): + if prefix.endswith('.'): + prefix = prefix[:-1] + Device.__init__(self, prefix, delim='.', attrs=self.attrs, **kwargs) + + diff --git a/epics/devices/bo.py b/epics/devices/bo.py new file mode 100644 index 0000000..83fe641 --- /dev/null +++ b/epics/devices/bo.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python +from .. import Device + +class bo(Device): + """ + Simple binary output device + """ + + attrs = ('DOL', 'OMSL', 'RVAL', 'HIGH', 'ZNAM', 'ONAM', 'VAL', 'EGU', + 'HOPR', 'LOPR', 'PREC', 'NAME', 'DESC', 'DTYP') + + def __init__(self, prefix, **kwargs): + if prefix.endswith('.'): + prefix = prefix[:-1] + Device.__init__(self, prefix, delim='.', attrs=self.attrs, **kwargs) diff --git a/epics/devices/mca.py b/epics/devices/mca.py new file mode 100644 index 0000000..5a29756 --- /dev/null +++ b/epics/devices/mca.py @@ -0,0 +1,607 @@ +#!/usr/bin/python +import sys +import time +import numpy as np +from .. import Device, get_pv, poll, caput, caget + +try: + from collections import OrderedDict +except: + from ordereddict import OrderedDict + +if sys.version[0] == '2': + from ConfigParser import ConfigParser +elif sys.version[0] == '3': + from configparser import ConfigParser + +MAX_ROIS = 32 +class DXP(Device): + _attrs = ('PreampGain','MaxEnergy','ADCPercentRule','BaselineCutPercent', + 'BaselineThreshold','BaselineFilterLength','BaselineCutEnable', + 'InputCountRate', 'OutputCountRate', + 'GapTime','PeakingTime','EnergyThreshold','MaxWidth', + 'PresetMode', 'TriggerPeakingTime', + 'TriggerGapTime','TriggerThreshold') + + def __init__(self,prefix,mca=1): + self._prefix = "%sdxp%i" % (prefix, mca) + Device.__init__(self, self._prefix, delim=':') + poll() + + +class ROI(Device): + """epics ROI device for MCA record + + >>> from epics.devices.mca import ROI, MCA + >>> r = ROI('PRE:mca1', roi=1) + >>> print r.name, r.left, r.right + + arguments + --------- + prefix MCA record prefix + roi integer for ROI (0 through 31) + bgr_width width in bins for calculating NET counts + data_pv optional PV name to read counts from (not needed + for most MCA records, but useful for some) + + attribute (read/write) + ---------------------- + LO, left low bin for ROI + HI, right high bin for ROI + NM, name name + + properties (read only) + ----------------------- + center roi center bin + width roi width (in bins) + address == prefix + total sum counts in ROI + net background-subtracted sum counts in ROI + + methods + ------- + clear remove ROI + """ + + _nonpvs = ('_prefix', '_pvs', '_delim', 'attrs', 'width', 'center', + 'bgr_width', 'address', 'net', 'total', '_dat_', '_net_') + _aliases = {'left': 'LO', 'right': 'HI', 'name': 'NM'} + def __init__(self, prefix, roi=0, bgr_width=3, data_pv=None): + self.address = self._prefix = '%s.R%i' % (prefix, roi) + self.bgr_width = bgr_width + _attrs = ('NM', 'LO', 'HI') + Device.__init__(self,self._prefix, delim='', + attrs=_attrs, aliases=self._aliases, + with_poll=False) + if data_pv is None: + data_pv = self.address + if isinstance(data_pv, basestring): + data_pv = get_pv(data_pv) + self._pvs['_dat_'] = data_pv + self._pvs['_net_'] = get_pv(self.address + 'N') + + def __eq__(self, other): + """used for comparisons""" + return (self.LO == getattr(other, 'LO', None) and + self.HI == getattr(other, 'HI', None) and + self.bgr_width == getattr(other, 'bgr_width', None) ) + + def __ne__(self, other): return not self.__eq__(other) + def __lt__(self, other): return self.LO < getattr(other, 'LO', None) + def __le__(self, other): return self.LO <= getattr(other, 'LO', None) + def __gt__(self, other): return self.LO > getattr(other, 'LO', None) + def __ge__(self, other): return self.LO >= getattr(other, 'LO', None) + + def __repr__(self): + "string representation" + pref = self._prefix + if pref.endswith('.'): + pref = pref[:-1] + return "" % (pref, self.NM, + self.LO, self.HI) + + #return "" % (pref, self.NM, + # self.LO, self.HI) + + @property + def total(self): + return self.get_counts(net=False) + + @property + def sum(self): + return self.get_counts(net=False) + + @property + def net(self): + return self.get_counts(net=True) + + @property + def center(self): + return int(round(self.HI + self.LO)/2.0) + + @property + def width(self): + return int(round(self.HI - self.LO)) + + def clear(self): + self.NM = '' + self.LO = -1 + self.HI = -1 + + def get_counts(self, data=None, net=False): + """ + calculate total and net counts for a spectra + + Parameters: + ----------- + * data: numpy array of spectra or None to read from PV + * net: bool to set net counts (default=False: total counts returned) + """ + # implicitly read data from a PV + if data is None and self._pvs['_dat_'] is not None: + data = self._pvs['_dat_'].get() + if net and not isinstance(data, np.ndarray): + data = self._pvs['_net_'].get() + if not isinstance(data, np.ndarray): + return data + + total = data[self.LO:self.HI+1].sum() + if not net: + return total + # calculate net counts + bgr_width = int(self.bgr_width) + ilmin = max((self.LO - bgr_width), 0) + irmax = min((self.HI + bgr_width), len(data)-1) + 1 + bgr_counts = np.concatenate((data[ilmin:self.LO], + data[self.HI+1:irmax])).mean() + + return (total - bgr_counts*(self.HI-self.LO)) + +class MCA(Device): + _attrs =('CALO', 'CALS', 'CALQ', 'TTH', 'EGU', + 'PRTM', 'PLTM', 'ACQG', 'NUSE', 'DWEL', + 'ERTM', 'ELTM', 'IDTIM') + _nonpvs = ('_prefix', '_pvs', '_delim', + '_npts', 'rois', '_nrois', 'rois') + + def __init__(self, prefix, mca=None, nrois=None, data_pv=None): + self._prefix = prefix + self._npts = None + self._nrois = nrois + if self._nrois is None: + self._nrois = MAX_ROIS + self.rois = [] + if isinstance(mca, int): + self._prefix = "%smca%i" % (prefix, mca) + + Device.__init__(self,self._prefix, delim='.', + attrs=self._attrs, with_poll=False) + self._pvs['VAL'] = get_pv("%sVAL" % self._prefix, auto_monitor=False) + + self._pvs['_dat_'] = None + if data_pv is not None: + self._pvs['_dat_'] = get_pv(data_pv) + poll() + + + def get_rois(self, nrois=None): + self.rois = [] + data_pv = self._pvs['_dat_'] + prefix = self._prefix + if prefix.endswith('.'): + prefix = prefix[:-1] + if nrois is None: + nrois = self._nrois + for i in range(nrois): + roi = ROI(prefix=prefix, roi=i, data_pv=data_pv) + if roi.NM is None: + break + if len(roi.NM.strip()) <= 0 or roi.HI <= 0: + break + self.rois.append(roi) + poll() + + return self.rois + + def del_roi(self, roiname): + self.get_rois() + for roi in self.rois: + if roi.NM.strip().lower() == roiname.strip().lower(): + roi.clear() + poll(0.010, 1.0) + self.set_rois(self.rois) + + def add_roi(self, roiname, lo=-1, hi=-1, calib=None): + """add an roi, given name, lo, and hi channels, and + an optional calibration other than that of this mca. + + That is, specifying an ROI with all of lo, hi AND calib + will set the ROI **by energy** so that it matches the + provided calibration. To add an ROI to several MCAs + with differing calibration, use + + cal_1 = mca1.get_calib() + for mca im (mca1, mca2, mca3, mca4): + mca.add_roi('Fe Ka', lo=600, hi=700, calib=cal_1) + """ + if lo < 0 or hi <0: + return + rois = self.get_rois() + try: + iroi = len(rois) + except: + iroi = 0 + if iroi >= MAX_ROIS: + raise ValueError('too many ROIs - cannot add more %i/%i' % (iroi, MAX_ROIS)) + data_pv = self._pvs['_dat_'] + prefix = self._prefix + if prefix.endswith('.'): prefix = prefix[:-1] + roi = ROI(prefix=prefix, roi=iroi, data_pv=self._pvs['_dat_']) + roi.NM = roiname.strip() + + offset, scale = 0.0, 1.0 + if calib is not None: + off, slope, quad = self.get_calib() + offset = calib[0] - off + scale = calib[1] / slope + + roi.LO = round(offset + scale*lo) + roi.HI = round(offset + scale*hi) + rois.append(roi) + self.set_rois(rois) + + def set_rois(self, rois, calib=None): + """set all rois, with optional calibration that those + ROIs correspond to (if they have a different energy + calibration), and ensures they are ordered and contiguous. + + A whole set of ROIs can be copied by energy from one mca + to another with: + + rois = mca1.get_rois() + calib = mca1.get_calib() + mca2.set_rois(rois, calib=calib) + """ + data_pv = self._pvs['_dat_'] + prefix = self._prefix + if prefix.endswith('.'): prefix = prefix[:-1] + + offset, scale = 0.0, 1.0 + if calib is not None: + off, slope, quad = self.get_calib() + offset = calib[0] - off + scale = calib[1] / slope + + # do an explicit get here to make sure all data is + # available before trying to sort it! + poll(0.0050, 1.0) + + [(r.get('NM'), r.get('LO')) for r in rois] + roidat = [(r.NM, r.LO, r.HI) for r in sorted(rois)] + + iroi = 0 + self.rois = [] + for name, lo, hi in roidat: + if len(name)<1 or lo<0 or hi<0: + continue + roi = ROI(prefix=prefix, roi=iroi, data_pv=data_pv) + roi.NM = name.strip() + roi.LO = round(offset + scale*lo) + roi.HI = round(offset + scale*hi) + self.rois.append(roi) + iroi += 1 + + # erase any remaning ROIs + for i in range(iroi, MAX_ROIS): + lo = caget("%s.R%iLO" % (prefix, i)) + if lo < 0: + break + caput("%s.R%iLO" % (prefix, i), -1) + caput("%s.R%iHI" % (prefix, i), -1) + caput("%s.R%iNM" % (prefix, i), '') + + def clear_rois(self, nrois=None): + for roi in self.get_rois(nrois=nrois): + roi.clear() + self.rois = [] + + def get_calib(self): + return [self.get(i) for i in ('CALO','CALS','CALQ')] + + def get_energy(self): + if self._npts is None: + self._npts = len(self.get('VAL')) + + en = np.arange(self._npts, dtype='f8') + cal = self.get_calib() + return cal[0] + en*(cal[1] + en*cal[2]) + + +class MultiXMAP(Device): + """ + multi-Channel XMAP DXP device + """ + + attrs = ['PresetReal','Dwell','Acquiring', 'EraseStart','StopAll', + 'PresetMode', 'PixelsPerBuffer_RBV', 'NextPixel', + 'PixelsPerRun', 'Apply', 'AutoApply', 'CollectMode', + 'SyncCount', 'BufferSize_RBV'] + + pathattrs = ('FilePath', 'FileTemplate', 'FileWriteMode', + 'FileName', 'FileNumber', 'FullFileName_RBV', + 'Capture', 'NumCapture', 'WriteFile_RBV', + 'AutoSave', 'EnableCallbacks', 'ArraySize0_RBV', + 'FileTemplate_RBV', 'FileName_RBV', 'AutoIncrement') + + _nonpvs = ('_prefix', '_pvs', '_delim', 'filesaver', + 'pathattrs', '_nonpvs', 'nmca', 'dxps', 'mcas') + + def __init__(self, prefix, filesaver='netCDF1:',nmca=4): + self.filesaver = filesaver + self._prefix = prefix + self.nmca = nmca + + self.dxps = [DXP(prefix, mca=i+1) for i in range(nmca)] + self.mcas = [MCA(prefix, mca=i+1) for i in range(nmca)] + + Device.__init__(self, prefix, attrs=self.attrs, + delim='', mutable=True) + for p in self.pathattrs: + pvname = '%s%s%s' % (prefix, filesaver, p) + self.add_pv(pvname, attr=p) + + def get_calib(self): + return [m.get_calib() for m in self.mcas] + + def get_rois(self): + return [m.get_rois() for m in self.mcas] + + def roi_calib_info(self): + buff = ['[rois]'] + add = buff.append + rois = self.get_rois() + for iroi in range(len(rois[0])): + name = rois[0][iroi].NM + s = [[rois[m][iroi].LO, rois[m][iroi].HI] for m in range(self.nmca)] + dat = repr(s).replace('],', '').replace('[', '').replace(']','').replace(',','') + add("ROI%2.2i = %s | %s" % (iroi, name, dat)) + + caldat = np.array(self.get_calib()) + add('[calibration]') + add("OFFSET = %s " % (' '.join(["%.7g" % i for i in caldat[:, 0]]))) + add("SLOPE = %s " % (' '.join(["%.7g" % i for i in caldat[:, 1]]))) + add("QUAD = %s " % (' '.join(["%.7g" % i for i in caldat[:, 2]]))) + + add('[dxp]') + for a in self.dxps[0]._attrs: + vals = [str(dxp.get(a, as_string=True)).replace(' ','_') for dxp in self.dxps] + add("%s = %s" % (a, ' '.join(vals))) + return buff + + def restore_rois(self, roifile): + """restore ROI setting from ROI.dat file""" + cp = ConfigParser() + cp.read(roifile) + rois = [] + self.mcas[0].clear_rois() + prefix = self.mcas[0]._prefix + if prefix.endswith('.'): + prefix = prefix[:-1] + iroi = 0 + for a in cp.options('rois'): + if a.lower().startswith('roi'): + name, dat = cp.get('rois', a).split('|') + lims = [int(i) for i in dat.split()] + lo, hi = lims[0], lims[1] + roi = ROI(prefix=prefix, roi=iroi) + roi.LO = lo + roi.HI = hi + roi.NM = name.strip() + rois.append(roi) + iroi += 1 + + poll(0.050, 1.0) + self.mcas[0].set_rois(rois) + cal0 = self.mcas[0].get_calib() + for mca in self.mcas[1:]: + mca.set_rois(rois, calib=cal0) + + def Write_CurrentConfig(self, filename=None): + buff = [] + add = buff.append + add('#Multi-Element xMAP Settings saved: %s' % time.ctime()) + add('[general]') + add('prefix= %s' % self._prefix) + add('nmcas = %i' % self.nmca) + add('filesaver= %s' % self.filesaver) + d.add('starting roi....') + buff.extend( self.roi_calib_info() ) + + d.add('wrote roi / calib / dxp') + + buff = '\n'.join(buff) + if filename is not None: + fh = open(filename,'w') + fh.write(buff) + fh.close() + d.add('wrote file') + # d.show() + return buff + + def start(self): + "Start Struck" + self.EraseStart = 1 + + if self.Acquiring == 0: + poll() + self.EraseStart = 1 + return self.EraseStart + + def stop(self): + "Stop Struck Collection" + self.StopAll = 1 + return self.StopAll + + def next_pixel(self): + "Advance to Next Pixel:" + self.NextPixel = 1 + return self.NextPixel + + def finish_pixels(self, timeout=2): + "Advance to Next Pixel until CurrentPixel == PixelsPerRun" + pprun = self.PixelsPerRun + cur = self.dxps[0].get('CurrentPixel') + t0 = time.time() + while cur < pprun and time.time()-t0 < timeout: + time.sleep(0.1) + pprun = self.PixelsPerRun + cur = self.dxps[0].get('CurrentPixel') + ok = cur >= pprun + if not ok: + print('XMAP needs to finish pixels ', cur, ' / ' , pprun) + for i in range(pprun-cur): + self.next_pixel() + time.sleep(0.10) + self.FileCaptureOff() + return ok, pprun-cur + + + def readmca(self,n=1): + "Read a Struck MCA" + return self.get('mca%i' % n) + + def SCAMode(self): + "put XMAP in SCA mapping mode" + self.CollectMode = 2 + + def SpectraMode(self): + "put XMAP in MCA spectra mode" + self.stop() + self.CollectMode = 0 + self.PresetMode = 0 + # wait until BufferSize is ready + buffsize = -1 + t0 = time.time() + while time.time() - t0 < 5: + self.CollectMode = 0 + time.sleep(0.05) + if self.BufferSize_RBV < 16384: + break + + def MCAMode(self, filename=None, filenumber=None, npulses=11): + "put XMAP in MCA mapping mode" + self.AutoApply = 1 + self.stop() + self.PresetMode = 0 + self.setFileWriteMode(2) + if npulses < 2: + npulses = 2 + self.CollectMode = 1 + self.PixelsPerRun = npulses + + # First, make sure ArraySize0_RBV for the netcdf plugin + # is the correct value + self.FileCaptureOff() + self.start() + f_size = -1 + t0 = time.time() + while (f_size < 16384) and time.time()-t0 < 10: + for i in range(5): + time.sleep(0.1) + self.NextPixel = 1 + f_size = self.fileGet('ArraySize0_RBV') + if f_size > 16384: + break + # + self.PixelsPerRun = npulses + self.SyncCount = 1 + + self.setFileNumber(filenumber) + if filename is not None: + self.setFileName(filename) + + # wait until BufferSize is ready + self.Apply = 1 + self.CollectMode = 1 + self.PixelsPerRun = npulses + time.sleep(0.50) + t0 = time.time() + while time.time() - t0 < 10: + time.sleep(0.25) + if self.BufferSize_RBV > 16384: + break + + # set expected number of buffers to put in a single file + ppbuff = self.PixelsPerBuffer_RBV + time.sleep(0.25) + if ppbuff is None: + ppbuff = 124 + self.setFileNumCapture(1 + (npulses-1)/ppbuff) + f_buffsize = -1 + t0 = time.time() + while time.time()- t0 < 5: + time.sleep(0.1) + f_buffsize = self.fileGet('ArraySize0_RBV') + if self.BufferSize_RBV == f_buffsize: + break + + time.sleep(0.5) + return + + def filePut(self,attr, value, **kw): + return self.put("%s%s" % (self.filesaver, attr), value, **kw) + + def fileGet(self, attr, **kw): + return self.get("%s%s" % (self.filesaver, attr), **kw) + + def setFilePath(self, pathname): + return self.filePut('FilePath', pathname) + + def setFileTemplate(self, fmt): + return self.filePut('FileTemplate', fmt) + + def setFileWriteMode(self, mode): + return self.filePut('FileWriteMode', mode) + + def setFileName(self, fname): + return self.filePut('FileName', fname) + + def nextFileNumber(self): + self.setFileNumber(1+self.fileGet('FileNumber')) + + def setFileNumber(self, fnum=None): + if fnum is None: + self.filePut('AutoIncrement', 1) + else: + self.filePut('AutoIncrement', 0) + return self.filePut('FileNumber',fnum) + + def getLastFileName(self): + return self.fileGet('FullFileName_RBV',as_string=True) + + def FileCaptureOn(self): + return self.filePut('Capture', 1) + + def FileCaptureOff(self): + return self.filePut('Capture', 0) + + def setFileNumCapture(self,n): + return self.filePut('NumCapture', n) + + def FileWriteComplete(self): + return (0==self.fileGet('WriteFile_RBV') ) + + def getFileTemplate(self): + return self.fileGet('FileTemplate_RBV',as_string=True) + + def getFileName(self): + return self.fileGet('FileName_RBV',as_string=True) + + def getFileNumber(self): + return self.fileGet('FileNumber_RBV') + + def getFilePath(self): + return self.fileGet('FilePath_RBV',as_string=True) + + def getFileNameByIndex(self,index): + return self.getFileTemplate() % (self.getFilePath(), self.getFileName(), index) diff --git a/epics/devices/ordereddict.py b/epics/devices/ordereddict.py new file mode 100644 index 0000000..72f8850 --- /dev/null +++ b/epics/devices/ordereddict.py @@ -0,0 +1,125 @@ +# Copyright (c) 2009 Raymond Hettinger +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + +from UserDict import DictMixin +class OrderedDict(dict, DictMixin): + def __init__(self, *args, **kwds): + if len(args) > 1: + raise TypeError('expected at most 1 arguments, got %d' % len(args)) + try: + self.__end + except AttributeError: + self.clear() + self.update(*args, **kwds) + + def clear(self): + self.__end = end = [] + end += [None, end, end] # sentinel node for doubly linked list + self.__map = {} # key --> [key, prev, next] + dict.clear(self) + + def __setitem__(self, key, value): + if key not in self: + end = self.__end + curr = end[1] + curr[2] = end[1] = self.__map[key] = [key, curr, end] + dict.__setitem__(self, key, value) + + def __delitem__(self, key): + dict.__delitem__(self, key) + key, prev, next = self.__map.pop(key) + prev[2] = next + next[1] = prev + + def __iter__(self): + end = self.__end + curr = end[2] + while curr is not end: + yield curr[0] + curr = curr[2] + + def __reversed__(self): + end = self.__end + curr = end[1] + while curr is not end: + yield curr[0] + curr = curr[1] + + def popitem(self, last=True): + if not self: + raise KeyError('dictionary is empty') + if last: + key = reversed(self).next() + else: + key = iter(self).next() + value = self.pop(key) + return key, value + + def __reduce__(self): + items = [[k, self[k]] for k in self] + tmp = self.__map, self.__end + del self.__map, self.__end + inst_dict = vars(self).copy() + self.__map, self.__end = tmp + if inst_dict: + return (self.__class__, (items,), inst_dict) + return self.__class__, (items,) + + def keys(self): + return list(self) + + setdefault = DictMixin.setdefault + update = DictMixin.update + pop = DictMixin.pop + values = DictMixin.values + items = DictMixin.items + iterkeys = DictMixin.iterkeys + itervalues = DictMixin.itervalues + iteritems = DictMixin.iteritems + + def __repr__(self): + if not self: + return '%s()' % (self.__class__.__name__,) + return '%s(%r)' % (self.__class__.__name__, self.items()) + + def copy(self): + return self.__class__(self) + + @classmethod + def fromkeys(cls, iterable, value=None): + d = cls() + for key in iterable: + d[key] = value + return d + + def __eq__(self, other): + if isinstance(other, OrderedDict): + if len(self) != len(other): + return False + for p, q in zip(self.items(), other.items()): + if p != q: + return False + return True + return dict.__eq__(self, other) + + def __ne__(self, other): + return not self == other diff --git a/epics/devices/scaler.py b/epics/devices/scaler.py new file mode 100755 index 0000000..dc4b1b4 --- /dev/null +++ b/epics/devices/scaler.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python +"""Epics Scaler""" +from .. import Device, poll + +class Scaler(Device): + """ + Simple implementation of SynApps Scaler Record. + """ + attrs = ('CNT', 'CONT', 'TP', 'T', 'VAL') + attr_kws = {'calc_enable': '%s_calcEnable.VAL'} + chan_attrs = ('NM%i', 'S%i') + calc_attrs = {'calc%i': '%s_calc%i.VAL', 'expr%i': '%s_calc%i.CALC'} + _nonpvs = ('_prefix', '_pvs', '_delim', '_nchan', '_chans') + + def __init__(self, prefix, nchan=8): + self._nchan = nchan + self._chans = range(1, nchan+1) + + attrs = list(self.attrs) + for i in self._chans: + for att in self.chan_attrs: + attrs.append(att % i) + + Device.__init__(self, prefix, delim='.', attrs=attrs) + + for key, val in self.attr_kws.items(): + self.add_pv(val % prefix, attr= key) + + for i in self._chans: + for key, val in self.calc_attrs.items(): + self.add_pv(val % (prefix, i), attr = key % i) + self._mutable = False + + def AutoCountMode(self): + "set to autocount mode" + self.put('CONT', 1) + + def OneShotMode(self): + "set to one shot mode" + self.put('CONT', 0) + + def CountTime(self, ctime): + "set count time" + self.put('TP', ctime) + + def Count(self, ctime=None, wait=False): + "set count, with optional counttime" + if ctime is not None: + self.CountTime(ctime) + self.put('CNT', 1, wait=wait) + poll() + + def EnableCalcs(self): + " enable calculations" + self.put('calc_enable', 1) + + def setCalc(self, i, calc): + "set the calculation for scaler i" + attr = 'expr%i' % i + self.put(attr, calc) + + def getNames(self): + "get all names" + return [self.get('NM%i' % i) for i in self._chans] + + def Read(self, use_calc=False): + "read all values" + attr = 'S%i' + if use_calc: + attr = 'calc%i' + return [self.get(attr % i) for i in self._chans] diff --git a/epics/devices/scan.py b/epics/devices/scan.py new file mode 100644 index 0000000..8c20c6a --- /dev/null +++ b/epics/devices/scan.py @@ -0,0 +1,342 @@ +#!/usr/bin/env python +""" +Epics scan record +""" +from .. import Device, poll +import threading + +NUM_POSITIONERS = 4 +NUM_TRIGGERS = 4 +NUM_DETECTORS = 70 + +class Scan(Device): + """ + A Device representing an Epics sscan record. + """ + + attrs = ('VAL', 'SMSG', 'CMND', 'NPTS', 'EXSC', 'NAME', 'PDLY', + 'PAUS', 'CPT', 'DDLY') + + pos_attrs = ('PV', 'SP', 'EP', 'SI', 'CP', 'WD', 'PA', 'AR', 'SM') + trig_attrs = ('PV', 'NV') + + _alias = {'device': 'P1PV', + 'start': 'P1SP', + 'end': 'P1EP', + 'step': 'P1SI', + 'table': 'P1PA', + 'absrel': 'P1AR', + 'mode': 'P1SM', + 'npts': 'NPTS', + 'execute': 'EXSC', + 'trigger': 'T1PV', + 'pause': 'PAUS', + 'current_point': 'CPT'} + + def __init__(self, name, **kwargs): + """ + Initialize the scan. + + name: The name of the scan record. + """ + attrs = list(self.attrs) + for i in range(1, NUM_POSITIONERS+1): + for a in self.pos_attrs: + attrs.append('P%i%s' % (i, a)) + for i in range(1, NUM_TRIGGERS+1): + for a in self.trig_attrs: + attrs.append('T%i%s' % (i, a)) + for i in range(1, NUM_DETECTORS+1): + attrs.append('D%2.2iPV' % i) + + self.waitSemaphore = threading.Semaphore(0) + Device.__init__(self, name, delim='.', attrs=attrs, **kwargs) + for attr, pv in Scan._alias.items(): + self.add_pv('%s.%s' % (name,pv), attr) + + # make sure this is really a sscan! + rectype = self.get('RTYP') + if rectype != 'sscan': + raise ScanException("%s is not an Epics Scan" % name) + + self.put('SMSG', '') + + def run(self, wait=False, timeout=86400): + """ + Execute the scan, optionally waiting for completion + + Arguments + --------- + wait whether to wait for completion, True/False (default False) + timeout maximum time to wait in seconds, default=86400 (1 day). + + """ + self.put('EXSC', 1, wait=wait, timeout=timeout) + + def _onDone(self, **kwargs): + if kwargs['value'] == 0: + self.waitSemaphore.release() + + def reset(self): + """Reset scan, clearing positioners, detectors, triggers""" + self.put('NPTS', 0) + for i in range(1, NUM_TRIGGERS+1): + self.clear_trigger(i) + for i in range(1, NUM_POSITIONERS+1): + self.clear_positioner(i) + for i in range(1, NUM_DETECTORS+1): + self.clear_detector(i) + poll(1.e-3, 1.0) + + def _print(self): + print('PV = %s' % self.get('P1PV')) + print('SP = %s' % self.get('P1SP')) + print('EP = %s' % self.get('P1EP')) + print('NPTS = %s' % self.get('NPTS')) + print('T = %s' % self.get('T1PV')) + + + def clear_detector(self, idet=1): + """completely clear a detector + + Arguments + --------- + idet index of detector (1 through 70, default 1) + """ + self.put("D%2.2iPV" % idet, '') + poll(1.e-3, 1.0) + + def add_detector(self, detector): + """add a detector to a scan definition + + Arguments + --------- + detector name of detector pv + + Returns + ------- + idet index of detector set + """ + idet = None + for _idet in range(1, NUM_DETECTORS+1): + poll(1.e-3, 1.0) + if len(self.get('D%2.2iPV' % _idet)) < 2: + idet = _idet + break + if idet is None: + raise ScanException("%i Detectors already defined." % (NUM_DETECTORS)) + self.put("D%2.2iPV" % idet, detector, wait=True) + return idet + + def clear_trigger(self, itrig=1): + """completely clear a trigger + + Arguments + --------- + itrig index of trigger (1 through 4, default 1) + """ + self.put("T%iPV" % itrig, '') + poll(1.e-3, 1.0) + + def add_trigger(self, trigger, value=1.0): + """add a trigger to a scan definition + + Arguments + --------- + trigger name of trigger pv + value value to send to trigger (default 1.0) + + Returns + ------- + itrig index of trigger set + """ + itrig = None + for _itrig in range(1, NUM_TRIGGERS+1): + poll(1.e-3, 1.0) + if len(self.get('T%iPV' % _itrig)) < 2: + itrig = _itrig + break + if itrig is None: + raise ScanException("%i Triggers already defined." % (NUM_TRIGGERS)) + + self.put("T%iPV" % itrig, trigger, wait=True) + self.put("T%iCD" % itrig, value, wait=True) + return itrig + + + def clear_positioner(self, ipos=1): + """completely clear a positioner + + Arguments + --------- + ipos index of positioner (1 through 4, default 1) + """ + for attr in self.pos_attrs: + nulval = 0 + if attr == 'PV': nulval = '' + if attr == 'PA': nulval = [0] + self.put("P%i%s" % (ipos, attr), nulval) + self.put("R%iPV" % ipos, '') + poll(1.e-3, 1.0) + + def add_positioner(self, drive, readback=None, + start=None, stop=None, step=None, + center=None, width=None, + mode='linear', absolute=True, array=None): + """add a positioner to a scan definition + + Arguments + ---------- + drive name of drive pv + readback name of readback pv (defaults to .RBV if drive ends in .VAL) + mode positioner mode ('linear', 'table', fly', default 'linear') + absolute whether to use absolute values (True/False, default True) + start start value + stop stop value + step step value + center center value + width width value + array array of values for table or fly mode + + Returns + ------- + ipos index of positioner set + + """ + ipos = None + for _ipos in range(1, NUM_POSITIONERS+1): + poll(1.e-3, 1.0) + if len(self.get('P%iPV' % _ipos)) < 2: + ipos = _ipos + break + if ipos is None: + raise ScanException("%i Positioners already defined." % (NUM_POSITIONERS)) + + self.put('P%iPV' % ipos, drive, wait=True) + if readback is None and drive.endswith('.VAL'): + readback = drive[:-4] + '.RBV' + if readback is not None: + self.put('R%iPV' % ipos, readback) + + # set relative/absolute + if absolute: + self.put('P%iAR' % ipos, 0) + else: + self.put('P%iAR' % ipos, 1) + + # set mode + smode = 0 + if mode.lower().startswith('table'): + smode = 1 + elif mode.lower().startswith('fly'): + smode = 2 + self.put('P%iSM' % ipos, smode) + + # start, stop, step, center, width + if start is not None: + self.put('P%iSP' % ipos, start) + if stop is not None: + self.put('P%iEP' % ipos, stop) + if step is not None: + self.put('P%iSI' % ipos, step) + if center is not None: + self.put('P%iCP' % ipos, center) + if width is not None: + self.put('P%iWD' % ipos, width) + + # table or fly mode + if smode in (1, 2) and array is not None: + self.put('P%iPA' % ipos, array) + poll(1.e-3, 1.0) + return ipos + + def set_positioner(self, ipos, drive=None, readback=None, + start=None, stop=None, step=None, + center=None, width=None, + mode=None, absolute=None, array=None): + """change a positioner setting in a scan definition + all settings are optional, and will leave other settings unchanged + + Arguments + ---------- + drive name of drive pv + readback name of readback pv + mode positioner mode ('linear', 'table', fly', default 'linear') + absolute whether to use absolute values (True/False, default True) + start start value + stop stop value + step step value + center center value + width width value + array array of values for table or fly mode + + Notes + ----- + This allows changing a scan, for example: + + s = Scan('XXX:scan1') + ipos1 = s.add_positioner('XXX:m1.VAL', start=-1, stop=1, step=0.1) + .... + + s.run() + + Then changing the scan definition with + + s.set_positioner(ipos1, start=0, stop=0.2, step=0.01) + s.run() + """ + if ipos is None: + raise ScanException("must give positioner index") + + if drive is not None: + self.put('P%iPV' % ipos, drive) + if readback is not None: + self.put('R%iPV' % ipos, readback) + if start is not None: + self.put('P%iSP' % ipos, start) + if stop is not None: + self.put('P%iEP' % ipos, stop) + if step is not None: + self.put('P%iSI' % ipos, step) + if center is not None: + self.put('P%iCP' % ipos, center) + if width is not None: + self.put('P%iWD' % ipos, width) + if array is not None: + self.put('P%iPA' % ipos, array) + + if absolute is not None: + if absolute: + self.put('P%iAR' % ipos, 0) + else: + self.put('P%iAR' % ipos, 1) + + if mode is not None: + smode = 0 + if mode.lower().startswith('table'): + smode = 1 + elif mode.lower().startswith('fly'): + smode = 2 + self.put('P%iSM' % ipos, smode) + poll(1.e-3, 1.0) + + + def after_scan(self, mode): + """set after scan mode""" + self.put("PASM", mode, wait=True) + + def positioner_delay(self, pdelay): + """set positioner delay in seconds""" + self.put("PDLY", pdelay, wait=True) + + def detector_delay(self, pdelay): + """set detector delay in seconds""" + self.put("DDLY", pdelay, wait=True) + +class ScanException(Exception): + """ raised to indicate a problem with a scan""" + def __init__(self, msg, *args): + Exception.__init__(self, *args) + self.msg = msg + def __str__(self): + return str(self.msg) diff --git a/epics/devices/srs570.py b/epics/devices/srs570.py new file mode 100755 index 0000000..a4811cc --- /dev/null +++ b/epics/devices/srs570.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python +"""Epics Support for +Stanford Research Systems 570 current amplifier +""" +from .. import Device + +VALID_STEPS = [1, 2, 5, 10, 20, 50, 100, 200, 500] +VALID_UNITS = ['pA/V', 'nA/V','uA/V', 'mA/V'] + +class SRS570(Device): + """ + SRS (Stanford Research Systems) 570 current amplifier + """ + + attrs = ('sens_num', 'sens_unit', 'offset_num', 'offset_unit', + 'offset_sign', 'offset_on' 'off_u_put', 'bias_put', + 'gain_mode', 'filter_type', 'invert_on', 'init.PROC') + + _nonpvs = ('_prefix', '_pvs', '_delim', '_nchan', '_chans') + + def __init__(self, prefix): + Device.__init__(self, prefix, delim='', + attrs=self.attrs, mutable=False) + self.initialize() + + def initialize(self, bias=0, gain_mode=0, filter_type=0, + invert=False): + """set initial values""" + inv_val = 0 + if invert: inv_val = 1 + self.put('gain_mode', gain_mode) # 0 = low noise + self.put('filter_type', filter_type) # 0 no filter + self.put('invert_on', inv_val) + self.put('bias_put', bias) + + def set_sensitivity(self, value, units, offset=None, + scale_offset=True): + "set sensitivity" + if value not in VALID_STEPS or units not in VALID_UNITS: + print('invalid input') + return + + ival = VALID_STEPS.index(value) + uval = VALID_UNITS.index(units) + + self.put('sens_num', ival) + self.put('sens_unit', uval) + if scale_offset: + # scale offset to by 0.1 x sensitivity + # i.e, a sensitivity of 200 nA/V should + # set set the input offset to 20 nA. + ioff = ival - 3 + uoff = uval + if ioff < 0: + ioff = ival + 6 + uoff = uval - 1 + self.put('offset_num', ioff) + self.put('offset_unit', uoff) + if offset is not None: + self.set_offset(offset) + self.put('init.PROC', 1) + + def set_offset(self, value): + self.put('off_u_put', value) + + def increase_sensitivity(self): + "increase sensitivity by 1 step" + snum = self.get('sens_num') + sunit = self.get('sens_unit') + if snum == 0: + snum = 9 + sunit = sunit - 1 + if sunit < 0: + # was at highest sensitivity + snum, sunit = 1, 0 + snum = snum - 1 + self.set_sensitivity(VALID_STEPS[snum], VALID_UNITS[sunit]) + + def decrease_sensitivity(self): + "decrease sensitivity by 1 step" + snum = self.get('sens_num') + sunit = self.get('sens_unit') + if snum == 8: + snum = -1 + sunit = sunit + 1 + if sunit > 3: + # was at lowest sensitivity + snum, sunit = 7, 3 + snum = snum + 1 + self.set_sensitivity(VALID_STEPS[snum], VALID_UNITS[sunit]) + diff --git a/epics/devices/struck.py b/epics/devices/struck.py new file mode 100755 index 0000000..5c32a52 --- /dev/null +++ b/epics/devices/struck.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python +import sys +import time +import copy +import numpy +from .. import Device +from .scaler import Scaler +from .mca import MCA + +HEADER = '''# Struck MCA data: %s +# Nchannels, Nmca = %i, %i +# Time in microseconds +#---------------------- +# %s +# %s +''' + +class Struck(Device): + """ + Very simple implementation of Struck SIS MultiChannelScaler + """ + attrs = ('ChannelAdvance', 'Prescale', 'EraseStart', + 'EraseAll', 'StartAll', 'StopAll', + 'PresetReal', 'ElapsedReal', + 'Dwell', 'Acquiring', 'NuseAll', + 'CurrentChannel', 'CountOnStart', # InitialChannelAdvance', + 'SoftwareChannelAdvance', 'Channel1Source', + 'ReadAll', 'DoReadAll', 'Model', 'Firmware') + + _nonpvs = ('_prefix', '_pvs', '_delim', '_nchan', + 'clockrate', 'scaler', 'mcas') + + def __init__(self, prefix, scaler=None, nchan=8, clockrate=50.0): + if not prefix.endswith(':'): + prefix = "%s:" % prefix + self._nchan = nchan + self.scaler = None + self.clockrate = clockrate # clock rate in MHz + + if scaler is not None: + self.scaler = Scaler(scaler, nchan=nchan) + + self.mcas = [] + for i in range(nchan): + self.mcas.append(MCA(prefix, mca=i+1, nrois=2)) + + Device.__init__(self, prefix, delim='', + attrs=self.attrs, mutable=False) + + def ExternalMode(self, countonstart=0, initialadvance=None, + realtime=0, prescale=1): + """put Struck in External Mode, with the following options: + option meaning default value + ---------------------------------------------------------- + countonstart set Count on Start 0 + initialadvance set Initial Channel Advance None + reatime set Preset Real Time 0 + prescale set Prescale value 1 + """ + out = self.put('ChannelAdvance', 1) # external + if self.scaler is not None: + self.scaler.OneShotMode() + if realtime is not None: + self.put('PresetReal', realtime) + if prescale is not None: + self.put('Prescale', prescale) + if countonstart is not None: + self.put('CountOnStart', countonstart) + if initialadvance is not None: + self.put('InitialChannelAdvancel', initialadvance) + + return out + + def InternalMode(self, prescale=None): + "put Struck in Internal Mode" + out = self.put('ChannelAdvance', 0) # internal + if self.scaler is not None: + self.scaler.OneShotMode() + if prescale is not None: + self.put('Prescale', prescale) + return out + + def setPresetReal(self, val): + "Set Preset Real Tiem" + return self.put('PresetReal', val) + + def setDwell(self, val): + "Set Dwell Time" + return self.put('Dwell', val) + + def AutoCountMode(self): + "set auto count mode" + if self.scaler is not None: + self.scaler.AutoCountMode() + + def start(self): + "Start Struck" + if self.scaler is not None: + self.scaler.OneShotMode() + return self.put('EraseStart', 1) + + def stop(self): + "Stop Struck Collection" + return self.put('StopAll', 1) + + def erase(self): + "Start Struck" + return self.put('EraseAll', 1) + + def mcaNread(self, nmca=1): + "Read a Struck MCA" + return self.get('mca%i.NORD' % nmca) + + def readmca(self, nmca=1, count=None): + "Read a Struck MCA" + return self.get('mca%i' % nmca, count=count) + + def read_all_mcas(self): + return [self.readmca(nmca=i+1) for i in range(self._nchan)] + + def saveMCAdata(self, fname='Struck.dat', mcas=None, + ignore_prefix=None, npts=None): + "save MCA spectra to ASCII file" + sdata, names, addrs = [], [], [] + npts = 1.e99 + time.sleep(0.005) + for nchan in range(self._nchan): + nmca = nchan + 1 + _name = 'MCA%i' % nmca + _addr = '%s.MCA%i' % (self._prefix, nmca) + time.sleep(0.002) + if self.scaler is not None: + scaler_name = self.scaler.get('NM%i' % nmca) + if scaler_name is not None: + _name = scaler_name.replace(' ', '_') + _addr = self.scaler._prefix + 'S%i' % nmca + mcadat = self.readmca(nmca=nmca) + npts = min(npts, len(mcadat)) + if len(_name) > 0 or sum(mcadat) > 0: + names.append(_name) + addrs.append(_addr) + sdata.append(mcadat) + + sdata = numpy.array([s[:npts] for s in sdata]).transpose() + sdata[:, 0] = sdata[:, 0]/self.clockrate + + nelem, nmca = sdata.shape + npts = min(nelem, npts) + + addrs = ' | '.join(addrs) + names = ' | '.join(names) + formt = '%9i ' * nmca + '\n' + + fout = open(fname, 'w') + fout.write(HEADER % (self._prefix, npts, nmca, addrs, names)) + for i in range(npts): + fout.write(formt % tuple(sdata[i])) + fout.close() + return (nmca, npts) + +if __name__ == '__main__': + strk = Struck('13IDE:SIS1:') + adv = 'ChannelAdvance' + sys.stdout.write("%s = %s\n" % (adv, strk.PV(adv).char_value)) diff --git a/epics/devices/transform.py b/epics/devices/transform.py new file mode 100644 index 0000000..fb4a245 --- /dev/null +++ b/epics/devices/transform.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python +"""Epics transform record""" +from .. import Device + +class Transform(Device): + "Epics transfrom record" + + attr_fmts = {'Value': '%s', + 'Input': 'INP%s', + 'Input_Valid': 'I%sV', + 'Expression': 'CLC%s', + 'Output': 'OUT%s', + 'Output_Valid': 'O%sV', + 'Comment': 'CMT%s', + 'Expression_Valid': 'C%sV', + 'Previous_Value': 'L%s'} + + rows = 'ABCDEFGHIJKLMNOP' + def __init__(self, prefix, **kwargs): + if prefix.endswith('.'): + prefix = prefix[:-1] + + self.attrs = ['COPT', 'PREC'] + for fmt in self.attr_fmts.values(): + for let in self.rows: + self.attrs.append(fmt % let) + + Device.__init__(self, prefix, delim='.', + attrs=self.attrs, **kwargs) + + def __validrow(self, row): + return (isinstance(row, (str, unicode)) and + len(row)==1 and row in self.rows) + + def get_row(self, row='A'): + """get full data for a calculation 'row' (or letter): + + returns dictionary with keywords (and PV suffix for row='B'): + + 'Value': B + 'Input': INPB + 'Input_Valid': IBV + 'Expression': CLCB + 'Output': OUTB + 'Output_Valid': OBV + 'Comment': CMTB + 'Expression_Valid': CBV + 'Previous_Value': LB + + """ + if not self.__validrow(row): + return None + dat = {} + for label, fmt in self.attr_fmts.items(): + dat[label] = self._pvs[fmt % row].get() + return dat + + def set_row(self, row='A', data=None): + """set full data for a calculation 'row' (or letter): + + data should be a dictionary as returned from get_row() + """ + if not self.__validrow(row): + return None + for key, value in data.items(): + if key in self.attr_fmts: + attr = self.attr_fmts[key] % row + if self._pvs[attr].write_access: + self._pvs[attr].put(value) + + def set_calc(self, row='A', calc=''): + """set calc for a 'row' (or letter): + calc should be a string""" + if not self.__validrow(row): + return None + self._pvs[self.attr_fmts['Expression'] % row].put(calc) + + def set_comment(self, row='A', comment=''): + """set comment for a 'row' (or letter): + comment should be a string""" + if not self.__validrow(row): + return None + self._pvs[self.attr_fmts['Comment'] % row].put(calc) + + def set_input(self, row='A', input=''): + """set input PV for a 'row' (or letter): + input should be a string""" + if not self.__validrow(row): + return None + self._pvs[self.attr_fmts['Input'] % row].put(calc) + + diff --git a/epics/devices/xspress3.py b/epics/devices/xspress3.py new file mode 100755 index 0000000..d81d11b --- /dev/null +++ b/epics/devices/xspress3.py @@ -0,0 +1,313 @@ +#!/usr/bin/python +import sys +import os +import time +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 +from epics.devices.ad_mca import ADMCA, ADMCAROI + +MAX_ROIS = 32 + +class ADFileMixin(object): + """mixin class for Xspress3""" + def filePut(self, attr, value, **kws): + return self.put("%s%s" % (self.filesaver, attr), value, **kws) + + def fileGet(self, attr, **kws): + return self.get("%s%s" % (self.filesaver, attr), **kws) + + def setFilePath(self, pathname): + fullpath = os.path.join(self.fileroot, pathname) + return self.filePut('FilePath', fullpath) + + def setFileTemplate(self,fmt): + return self.filePut('FileTemplate', fmt) + + def setFileWriteMode(self,mode): + return self.filePut('FileWriteMode', mode) + + def setFileName(self,fname): + return self.filePut('FileName', fname) + + def nextFileNumber(self): + self.setFileNumber(1+self.fileGet('FileNumber')) + + def setFileNumber(self, fnum=None): + if fnum is None: + self.filePut('AutoIncrement', 1) + else: + self.filePut('AutoIncrement', 0) + return self.filePut('FileNumber',fnum) + + def getLastFileName(self): + return self.fileGet('FullFileName_RBV',as_string=True) + + def FileCaptureOn(self): + return self.filePut('Capture', 1) + + def FileCaptureOff(self): + return self.filePut('Capture', 0) + + def setFileNumCapture(self,n): + return self.filePut('NumCapture', n) + + def FileWriteComplete(self): + return (0==self.fileGet('WriteFile_RBV') ) + + def getFileTemplate(self): + return self.fileGet('FileTemplate_RBV',as_string=True) + + def getFileName(self): + return self.fileGet('FileName_RBV',as_string=True) + + def getFileNumber(self): + return self.fileGet('FileNumber_RBV') + + def getFilePath(self): + return self.fileGet('FilePath_RBV',as_string=True) + + def getFileNameByIndex(self,index): + return self.getFileTemplate() % (self.getFilePath(), self.getFileName(), index) + +class Xspress3BaseMixin(object): + """xspress3 mixin -- triggers, acquire, etc""" + def useExternalTrigger(self): + self.TriggerMode = 3 + + def useInternalTrigger(self): + self.TriggerMode = 1 + + def setTriggerMode(self, mode): + self.TriggerMode = mode + + def start(self, capture=True): + time.sleep(.05) + if capture: + self.FileCaptureOn() + self.Acquire = 1 + + def stop(self): + self.Acquire = 0 + self.FileCaptureOff() + + def get_rois(self): + return [m.get_rois() for m in self.mcas] + +class Xspress3(Device, ADFileMixin, Xspress3BaseMixin): + """Epics Xspress3.20 interface (with areaDetector2)""" + + det_attrs = ('NumImages', 'NumImages_RBV', 'Acquire', 'Acquire_RBV', + 'ArrayCounter_RBV', 'ERASE', 'UPDATE', 'AcquireTime', + 'TriggerMode', 'StatusMessage_RBV', 'DetectorState_RBV') + + _nonpvs = ('_prefix', '_pvs', '_delim', 'filesaver', 'fileroot', + 'pathattrs', '_nonpvs', 'nmca', 'mcas') + + pathattrs = ('FilePath', 'FileTemplate', 'FileName', 'FileNumber', + 'Capture', 'NumCapture') + + def __init__(self, prefix, nmca=4, filesaver='HDF1:', + fileroot='/home/xspress3/cars5/Data'): + if not prefix.endswith(':'): + prefix = "%s:" % prefix + self.nmca = nmca + attrs = [] + attrs.extend(['%s%s' % (filesaver,p) for p in self.pathattrs]) + + self.filesaver = filesaver + self.fileroot = fileroot + self._prefix = prefix + self.mcas = [] + for i in range(nmca): + imca = i+1 + dprefix = "%sdet1:" % prefix + rprefix = "%sMCA%iROI" % (prefix, imca) + data_pv = "%sMCA%i:ArrayData" % (prefix, imca) + mca = ADMCA(dprefix, data_pv=data_pv, roi_prefix=rprefix) + self.mcas.append(mca) + + Device.__init__(self, prefix, attrs=attrs, delim='') + for attr in self.det_attrs: + self.add_pv("%sdet1:%s" % (prefix, attr), attr) + for i in range(nmca): + imca = i+1 + for j in range(8): + isca = j+1 + attr="C%iSCA%i"% (imca, isca) + self.add_pv("%s%s:Value_RBV" % (prefix, attr), attr) + for attr in ('TSNumPoints', 'TSControl'): + self.add_pv("%sMCA%iROI:%s" % (prefix, imca, attr), + "MCA%i%s" % (imca, attr)) + self.add_pv("%sC%iSCA:%s" % (prefix, imca, attr), + "SCA%i%s" % (imca, attr)) + time.sleep(0.05) + + def TimeSeriesCaptureOn(self, npts=None): + """ turns on a Time Series Capture""" + for imca in range(len(self.mcas)): + if npts is not None: + self._pvs["MCA%iTSNumPoints" % (imca+1)].put(npts) + self._pvs["SCA%iTSNumPoints" % (imca+1)].put(npts) + time.sleep(0.025) + for imca in range(len(self.mcas)): + self._pvs["MCA%iTSControl" % (imca+1)].put(0) + self._pvs["SCA%iTSControl" % (imca+1)].put(0) + + def TimeSeriesCaptureOff(self): + """ turns off a Time Series Capture""" + for imca in range(len(self.mcas)): + self._pvs["MCA%iTSControl" % (imca+1)].put(2) + self._pvs["SCA%iTSControl" % (imca+1)].put(2) + + def roi_calib_info(self): + buff = ['[rois]'] + add = buff.append + rois = self.mcas[0].get_rois() + for iroi, roi in enumerate(rois): + name = roi.Name + hi = roi.MinX + roi.SizeX + if len(name.strip()) > 0 and hi > 0: + dbuff = [] + for m in range(self.nmca): + dbuff.extend([roi.MinX, roi.MinX+roi.SizeX]) + dbuff = ' '.join([str(i) for i in dbuff]) + add("ROI%2.2i = %s | %s" % (iroi, name, dbuff)) + + add('[calibration]') + add("OFFSET = %s " % (' '.join(["0.000 "] * self.nmca))) + add("SLOPE = %s " % (' '.join(["0.010 "] * self.nmca))) + add("QUAD = %s " % (' '.join(["0.000 "] * self.nmca))) + add('[dxp]') + return buff + + def restore_rois(self, roifile): + """restore ROI setting from ROI.dat file""" + cp = ConfigParser() + cp.read(roifile) + roidat = [] + iroi = 0 + for a in cp.options('rois'): + if a.lower().startswith('roi'): + name, dat = cp.get('rois', a).split('|') + lims = [int(i) for i in dat.split()] + lo, hi = lims[0], lims[1] + roidat.append((name.strip(), lo, hi)) + + for mca in self.mcas: + mca.set_rois(roidat) + +class Xspress310(Device, ADFileMixin, Xspress3BaseMixin): + """Epics Xspress3.10 interface (older version)""" + attrs = ('NumImages', 'NumImages_RBV', + 'Acquire', 'Acquire_RBV', + 'ArrayCounter_RBV', + 'ERASE', 'UPDATE', 'AcquireTime', + 'TriggerMode', 'StatusMessage_RBV', + 'DetectorState_RBV') + + _nonpvs = ('_prefix', '_pvs', '_delim', 'filesaver', + 'fileroot', 'pathattrs', '_nonpvs', '_save_rois', + 'nmca', 'dxps', 'mcas') + + pathattrs = ('FilePath', 'FileTemplate', + 'FileName', 'FileNumber', + 'Capture', 'NumCapture') + + def __init__(self, prefix, nmca=4, filesaver='HDF5:', + fileroot='/home/xspress3/cars5/Data'): + self.nmca = nmca + attrs = list(self.attrs) + attrs.extend(['%s%s' % (filesaver,p) for p in self.pathattrs]) + + self.filesaver = filesaver + self.fileroot = fileroot + self._prefix = prefix + self._save_rois = [] + self.mcas = [MCA(prefix, mca=i+1) for i in range(nmca)] + + Device.__init__(self, prefix, attrs=attrs, delim='') + time.sleep(0.1) + + + def select_rois_to_save(self, roilist): + """copy rois from MCA record to arrays to be saved + by XSPress3""" + roilist = list(roilist) + if len(roilist) < 4: roilist.append((50, 4050)) + pref = self._prefix + self._save_rois = [] + for iroi, roiname in enumerate(roilist): + label = roiname + if isinstance(roiname, tuple): + lo, hi = roiname + label = '[%i:%i]' % (lo, hi) + else: + rname = roiname.lower().strip() + lo, hi = 50, 4050 + for ix in range(MAX_ROIS): + nm = caget('%smca1.R%iNM' % (pref, ix)) + if nm.lower().strip() == rname: + lo = caget('%smca1.R%iLO' % (pref, ix)) + hi = caget('%smca1.R%iHI' % (pref, ix)) + break + self._save_rois.append(label) + for imca in range(1, self.nmca+1): + pv_lo = "%sC%i_MCA_ROI%i_LLM" % (pref, imca, iroi+1) + pv_hi = "%sC%i_MCA_ROI%i_HLM" % (pref, imca, iroi+1) + caput(pv_hi, hi) + caput(pv_lo, lo) + + def roi_calib_info(self): + buff = ['[rois]'] + add = buff.append + rois = self.get_rois() + for iroi in range(len(rois[0])): + name = rois[0][iroi].NM + hi = rois[0][iroi].HI + if len(name.strip()) > 0 and hi > 0: + dbuff = [] + for m in range(self.nmca): + dbuff.extend([rois[m][iroi].LO, rois[m][iroi].HI]) + dbuff = ' '.join([str(i) for i in dbuff]) + add("ROI%2.2i = %s | %s" % (iroi, name, dbuff)) + + add('[calibration]') + add("OFFSET = %s " % (' '.join(["0.000 "] * self.nmca))) + add("SLOPE = %s " % (' '.join(["0.010 "] * self.nmca))) + add("QUAD = %s " % (' '.join(["0.000 "] * self.nmca))) + add('[dxp]') + return buff + + def restore_rois(self, roifile): + """restore ROI setting from ROI.dat file""" + cp = ConfigParser() + cp.read(roifile) + rois = [] + self.mcas[0].clear_rois() + prefix = self.mcas[0]._prefix + if prefix.endswith('.'): + prefix = prefix[:-1] + iroi = 0 + for a in cp.options('rois'): + if a.lower().startswith('roi'): + name, dat = cp.get('rois', a).split('|') + lims = [int(i) for i in dat.split()] + lo, hi = lims[0], lims[1] + # print('ROI ', name, lo, hi) + roi = ROI(prefix=prefix, roi=iroi) + roi.LO = lo + roi.HI = hi + roi.NM = name.strip() + rois.append(roi) + iroi += 1 + + poll(0.050, 1.0) + self.mcas[0].set_rois(rois) + cal0 = self.mcas[0].get_calib() + for mca in self.mcas[1:]: + mca.set_rois(rois, calib=cal0) diff --git a/epics/motor.py b/epics/motor.py new file mode 100644 index 0000000..072d517 --- /dev/null +++ b/epics/motor.py @@ -0,0 +1,643 @@ +#!/usr/bin/env python +""" + This module provides support for the EPICS motor record. +""" +# +# Author: Mark Rivers / Matt Newville +# Created: Sept. 16, 2002 +# Modifications: +# Oct 15, 2010 MN +# API Change, fuller inttegration with epics.Device, +# much simpler interface +# m = Motor('XXX:m1') +# print m.get_field('drive') # mapped to .VAL +# becomes +# m = Motor('XXX:m1') +# print m.VAL +# print m.drive # now an alias to 'VAL' +# +# 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. +# 3. make sure the motor is a motor object, else +# raise a MotorException. +# May 11, 2003 MN +# 1. added get_pv(attribute) to return PV for attribute +# 2. added __check_attr_stored(attr) method to +# consolidate checking if a PV is currently stored. +# Feb 27, 2003 M Newville altered EpicsMotor: +# 1. uses the EpicsCA.PV class, which automatically +# uses monitors to efficiently determine when to +# get PV values from the IOC +# 2. increase the number of Motor attributes. +# 3. increase the number of 'virtual attributes'. +# For example, +# >>>m = EpicsMotor('13BMD:m38') +# >>>m.drive = 20. +# causes the motor to move to 20 (user units). +# + +import sys +import time + +from . import ca +from . import device + +class MotorLimitException(Exception): + """ raised to indicate a motor limit has been reached """ + def __init__(self, msg, *args): + Exception.__init__(self, *args) + self.msg = msg + def __str__(self): + return str(self.msg) + +class MotorException(Exception): + """ raised to indicate a problem with a motor""" + def __init__(self, msg, *args): + Exception.__init__(self, *args) + self.msg = msg + def __str__(self): + return str(self.msg) + +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 + + Virtual attributes: + 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. + + Attribute Description Field + --------- ----------------------- ----- + drive Motor Drive Value .VAL + 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 + description Description of motor .DESC + resolution Resolution (units/step) .MRES + high_limit High soft limit (user) .HLM + low_limit Low soft limit (user) .LLM + dial_high_limit High soft limit (dial) .DHLM + dial_low_limit Low soft limit (dial) .DLLM + 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 + ignore_limits=True keyword set. + + Example use: + from epics import Motor + m = Motor('13BMD:m38') + m.move(10) # Move to position 10 in user coordinates + m.move(100, dial=True) # Move to position 100 in dial coordinates + m.move(1, step=True, relative=True) # Move 1 step relative to current position + + m.stop() # Stop moving immediately + high = m.high_limit # Get the high soft limit in user coordinates + m.dial_high_limit = 100 # Set the high limit to 100 in dial coodinates + speed = m.slew_speed # Get the slew speed + m.acceleration = 0.1 # Set the acceleration to 0.1 seconds + p=m.get_position() # Get the desired motor position in user coordinates + p=m.get_position(dial=1) # Get the desired motor position in dial coordinates + p=m.get_position(readback=1) # Get the actual position in user coordinates + p=m.get_position(readback=1, step=1) Get the actual motor position in steps + p=m.set_position(100) # Set the current position to 100 in user coordinates + # Puts motor in Set mode, writes value, puts back in Use mode. + p=m.set_position(10000, step=1) # Set the current position to 10000 steps + + """ + # parameter name (short), PV suffix, longer description + + # + _extras = { + 'disabled': '_able.VAL', } + + _alias = { + 'acceleration': 'ACCL', + 'back_accel': 'BACC', + 'backlash': 'BDST', + 'back_speed': 'BVEL', + 'card': 'CARD', + 'dial_high_limit': 'DHLM', + 'direction': 'DIR', + 'dial_low_limit': 'DLLM', + 'settle_time': 'DLY', + 'done_moving': 'DMOV', + 'dial_readback': 'DRBV', + 'description': 'DESC', + 'dial_drive': 'DVAL', + 'units': 'EGU', + 'encoder_step': 'ERES', + 'freeze_offset': 'FOFF', + 'move_fraction': 'FRAC', + 'hi_severity': 'HHSV', + 'hi_alarm': 'HIGH', + 'hihi_alarm': 'HIHI', + 'high_limit': 'HLM', + 'high_limit_set': 'HLS', + 'hw_limit': 'HLSV', + 'home_forward': 'HOMF', + 'home_reverse': 'HOMR', + 'high_op_range': 'HOPR', + 'high_severity': 'HSV', + 'integral_gain': 'ICOF', + 'jog_accel': 'JAR', + 'jog_forward': 'JOGF', + 'jog_reverse': 'JOGR', + 'jog_speed': 'JVEL', + 'last_dial_val': 'LDVL', + 'low_limit': 'LLM', + 'low_limit_set': 'LLS', + 'lo_severity': 'LLSV', + 'lolo_alarm': 'LOLO', + 'low_op_range': 'LOPR', + 'low_alarm': 'LOW', + 'last_rel_val': 'LRLV', + 'last_dial_drive': 'LRVL', + 'last_SPMG': 'LSPG', + 'low_severity': 'LSV', + 'last_drive': 'LVAL', + 'soft_limit': 'LVIO', + 'in_progress': 'MIP', + 'missed': 'MISS', + 'moving': 'MOVN', + 'resolution': 'MRES', + 'motor_status': 'MSTA', + 'offset': 'OFF', + 'output_mode': 'OMSL', + 'output': 'OUT', + 'prop_gain': 'PCOF', + 'precision': 'PREC', + 'readback': 'RBV', + 'retry_max': 'RTRY', + 'retry_count': 'RCNT', + 'retry_deadband': 'RDBD', + 'dial_difference': 'RDIF', + 'raw_encoder_pos': 'REP', + 'raw_high_limit': 'RHLS', + 'raw_low_limit': 'RLLS', + 'relative_value': 'RLV', + 'raw_motor_pos': 'RMP', + 'raw_readback': 'RRBV', + 'readback_res': 'RRES', + 'raw_drive': 'RVAL', + 'dial_speed': 'RVEL', + 's_speed': 'S', + 's_back_speed': 'SBAK', + 's_base_speed': 'SBAS', + 's_max_speed': 'SMAX', + 'set': 'SET', + 'stop_go': 'SPMG', + 's_revolutions': 'SREV', + 'stop_command': 'STOP', + 't_direction': 'TDIR', + 'tweak_forward': 'TWF', + 'tweak_reverse': 'TWR', + 'tweak_val': 'TWV', + 'use_encoder': 'UEIP', + 'u_revolutions': 'UREV', + 'use_rdbl': 'URIP', + 'drive': 'VAL', + 'base_speed': 'VBAS', + 'slew_speed': 'VELO', + 'version': 'VERS', + 'max_speed': 'VMAX', + 'use_home': 'ATHM', + 'deriv_gain': 'DCOF', + 'use_torque': 'CNEN', + 'device_type': 'DTYP', + 'record_type': 'RTYP', + 'status': 'STAT'} + + _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") + + if name.endswith('.VAL'): + name = name[:-4] + if name.endswith('.'): + name = name[:-1] + + self._prefix = name + 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': + raise MotorException("%s is not an Epics Motor" % name) + + for key, val in self._extras.items(): + pvname = "%s%s" % (name, val) + self.add_pv(pvname, attr=key) + self._callbacks = {} + + def __repr__(self): + return "" % (self._prefix, self.DESC) + + def __str__(self): + return self.__repr__() + + def __getattr__(self, attr): + " internal method " + if attr in self._alias: + attr = self._alias[attr] + if attr in self._pvs: + return self.get(attr) + if not attr.startswith('__'): + pv = self.PV(attr, connect=True) + if not pv.connected: + raise MotorException("EpicsMotor has no attribute %s" % attr) + return self.get(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 + 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 self._init: + try: + self.PV(attr) + return self.put(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 + raises expection if a limit is violated""" + for field, msg in (('LVIO', 'Soft limit violation'), + ('HLS', 'High hard limit violation'), + ('LLS', 'Low hard limit violation')): + 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)""" + ll_name, hl_name = 'LLM', 'HLM' + if dial: + ll_name, hl_name = 'DLLM', 'DHLM' + return (val <= self.get(hl_name) and val >= self.get(ll_name)) + + def move(self, val=None, relative=False, wait=False, timeout=300.0, + dial=False, step=False, raw=False, + ignore_limits=False, confirm_move=False): + """ moves motor drive to position + + arguments: + ========== + val value to move to (float) [Must be provided] + relative move relative to current position (T/F) [F] + wait whether to wait for move to complete (T/F) [F] + dial use dial coordinates (T/F) [F] + raw use raw coordinates (T/F) [F] + step use raw coordinates (backward compat)(T/F) [F] + ignore_limits try move without regard to limits (T/F) [F] + confirm_move try to confirm that move has begun (T/F) [F] + timeout max time for move to complete (in seconds) [300] + + return values: + -13 : invalid value (cannot convert to float). Move not attempted. + -12 : target value outside soft limits. Move not attempted. + -11 : drive PV is not connected: Move not attempted. + -8 : move started, but timed-out. + -7 : move started, timed-out, but appears done. + -5 : move started, unexpected return value from PV.put() + -4 : move-with-wait finished, soft limit violation seen + -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 + 3 : move-without-wait finished, hard limit violation seen + 4 : move-without-wait finished, soft limit violation seen + + """ + step = step or raw + + NONFLOAT, OUTSIDE_LIMITS, UNCONNECTED = -13, -12, -11 + TIMEOUT, TIMEOUT_BUTDONE = -8, -7 + UNKNOWN_ERROR = -5 + DONEW_SOFTLIM, DONEW_HARDLIM = -4, -3 + DONE_OK = 0 + MOVE_BEGUN, MOVE_BEGUN_CONFIRMED = 0, 1 + NOWAIT_SOFTLIM, NOWAIT_HARDLIM = 4, 3 + try: + val = float(val) + except TypeError: + return NONFLOAT + + drv, rbv = ('VAL', 'RBV') + if dial: + drv, rbv = ('DVAL', 'DRBV') + elif step: + drv, rbv = ('RVAL', 'RRBV') + + if relative: + val += self.get(drv) + + # Check for limit violations + if not ignore_limits and not step: + if not self.within_limits(val, dial=dial): + return OUTSIDE_LIMITS + + 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 + return TIMEOUT_BUTDONE + if 1 == stat: + if wait: # ... and finished OK + if 1 == self.get('LVIO'): + return DONEW_SOFTLIM + elif 1 == self.get('HLS') or 1 == self.get('LLS'): + return DONEW_HARDLIM + return DONE_OK + else: + if 1 == self.get('LVIO') or confirm_move: + ca.poll(evt=1.e-2) + moving = False + if confirm_move: + t0 = time.time() + while self.get('MOVN')==0: + ca.poll(evt=1.e-3) + if time.time() - t0 > 0.25: break + if 1 == self.get('MOVN'): + return MOVE_BEGUN_CONFIRMED + elif 1 == self.get('LVIO'): + return NOWAIT_SOFTLIM + elif 1 == self.get('HLS') or 1 == self.get('LLS'): + return NOWAIT_HARDLIM + else: + return MOVE_BEGUN + return UNKNOWN_ERROR + + + 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 + coordinates. + + Examples: + m=epicsMotor('13BMD:m38') + m.move(10) # Move to position 10 in user coordinates + p=m.get_position(dial=True) # Read the target position in dial coordinates + p=m.get_position(readback=True, step=True) # Read the actual position in steps + """ + pos, rbv = ('VAL','RBV') + if dial: + pos, rbv = ('DVAL', 'DRBV') + elif step or raw: + pos, rbv = ('RVAL', 'RRBV') + 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: + ret = 0 + if stat == -2: + ret = -1 + try: + self.check_limits() + except MotorLimitException: + 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 + # dial coordinates + m.set_position(1000, raw=True) # Set the motor position to 1000 steps + """ + + # Put the motor in "SET" mode + self.put('SET', 1) + + # determine which drive value to use + drv = 'VAL' + if dial: + drv = 'DVAL' + elif step or raw: + 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) + + def clear_callback(self, attr='drive'): + "clears callback for attribute" + try: + index = self._callbacks.get(attr, None) + if index is not None: + self.PV(attr).remove_callback(index=index) + except: + self.PV(attr).clear_callbacks() + + def set_callback(self, attr='VAL', callback=None, kws=None): + "define a callback for an attribute" + self.get(attr) + kw_args = {} + kw_args['motor_field'] = attr + if kws is not None: + kw_args.update(kws) + + index = self.PV(attr).add_callback(callback=callback, **kw_args) + self._callbacks[attr] = index + + def refresh(self): + """ refresh all motor parameters currently in use: + make sure all used attributes are up-to-date.""" + ca.poll() + + def StopNow(self): + "stop motor as soon as possible" + self.stop() + + 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""" + + if maxstep is None: + maxstep = 0.6 * abs(self.HLM - self.LLM) + steplist = [] + for i in range(decades): + for step in [j* 10**(i - self.PREC) for j in (1, 2, 5)]: + 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', + '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() + out = [] + out.append(repr(self)) + out.append( "--------------------------------------") + for nam, val in self.get_info().items(): + if len(nam) < 16: + nam = "%s%s" % (nam, ' '*(16-len(nam))) + out.append("%s = %s" % (nam, val)) + out.append("--------------------------------------") + ca.write("\n".join(out)) + + def show_all(self): + """ show all motor attributes""" + out = [] + add = out.append + add("# Motor %s" % (self._prefix)) + add("# field value PV name") + add("#------------------------------------------------------------") + ca.poll() + klist = list( self._alias.keys()) + klist.sort() + for attr in klist: + suff = self._alias[attr] + # pvn = self._alias[attr] + label = attr + ' '*(18-min(18, len(attr))) + value = self.get(suff, as_string=True) + pvname = self.PV(suff).pvname + if value is None: + value = 'Not Connected??' + 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__'): + for arg in sys.argv[1:]: + m = Motor(arg) + m.show_info() diff --git a/epics/multiproc.py b/epics/multiproc.py new file mode 100644 index 0000000..f22b5f5 --- /dev/null +++ b/epics/multiproc.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python +""" +Provides CAProcess, a multiprocessing.Process that correctly handles Channel Access +and CAPool, pool of CAProcesses + +Use CAProcess in place of multiprocessing.Process if your process will be calling +Channel Access or using Epics process variables + + from epics import (CAProcess, CAPool) + +""" +# +# Author: Ken Lauer +# Created: Feb. 27, 2014 +# Modifications: Matt Newville, changed to subclass multiprocessing.Process +# 3/28/2014 KL, added CAPool + +import multiprocessing as mp +from multiprocessing.pool import Pool +from . import ca +from .ca import clear_cache + + +__all__ = ['CAProcess', 'CAPool', 'clear_ca_cache'] + +class CAProcess(mp.Process): + """ + A Channel-Access aware (and safe) subclass of multiprocessing.Process + + Use CAProcess in place of multiprocessing.Process if your Process will + be doing CA calls! + """ + def __init__(self, **kws): + mp.Process.__init__(self, **kws) + + def run(self): + ca.initial_context = None + clear_cache() + mp.Process.run(self) + + +class CAPool(Pool): + """ + An EPICS-aware multiprocessing.Pool of CAProcesses. + """ + def __init__(self, *args, **kwargs): + self.Process = CAProcess + + Pool.__init__(self, *args, **kwargs) diff --git a/epics/pv.py b/epics/pv.py new file mode 100755 index 0000000..9fe5770 --- /dev/null +++ b/epics/pv.py @@ -0,0 +1,1129 @@ +#!/usr/bin/env python +# M Newville +# The University of Chicago, 2010 +# Epics Open License + +""" + Epics Process Variable +""" +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 _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' + + 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) + + if thispv is None: + 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: + 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: + tstamp = time.time() + tstamp, frac = divmod(tstamp, 1) + return "%s.%5.5i" % (time.strftime("%Y-%m-%d %H:%M:%S", + time.localtime(tstamp)), + round(1.e5*frac)) + + +class PV(object): + """Epics Process Variable + + A PV encapsulates an Epics Process Variable. + + The primary interface methods for a pv are to get() and put() is value:: + + >>> p = PV(pv_name) # create a pv object given a pv name + >>> p.get() # get pv value + >>> p.put(val) # set pv to specified value. + + Additional important attributes include:: + + >>> p.pvname # name of pv + >>> p.value # pv value (can be set or get) + >>> p.char_value # string representation of pv value + >>> p.count # number of elements in array pvs + >>> p.type # EPICS data type: 'string','double','enum','long',.. +""" + + _fmtsca = "" + _fmtarr = "" + _fields = ('pvname', 'value', 'char_value', 'status', 'ftype', 'chid', + 'host', 'count', 'access', 'write_access', 'read_access', + 'severity', 'timestamp', 'posixseconds', 'nanoseconds', + 'precision', 'units', 'enum_strs', + 'upper_disp_limit', 'lower_disp_limit', 'upper_alarm_limit', + 'lower_alarm_limit', 'lower_warning_limit', + 'upper_warning_limit', 'upper_ctrl_limit', 'lower_ctrl_limit') + + def __init__(self, pvname, callback=None, form='time', + verbose=False, auto_monitor=None, count= None, + connection_callback=None, + connection_timeout=None, + access_callback=None): + + self.pvname = pvname.strip() + self.form = form.lower() + self.verbose = verbose + 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) + self._args['pvname'] = self.pvname + self._args['count'] = count + self._args['nelm'] = -1 + self._args['type'] = 'unknown' + self._args['typefull'] = 'unknown' + self._args['access'] = 'unknown' + self.connection_callbacks = [] + + if connection_callback is not None: + self.connection_callbacks = [connection_callback] + + self.access_callbacks = [] + if access_callback is not None: + 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 callable(thiscb): + self.callbacks[i] = (thiscb, {}) + elif callable(callback): + self.callbacks[0] = (callback, {}) + + self.chid = None + if ca.current_context() is None: + ca.use_initial_context() + self.context = ca.current_context() + + self._args['chid'] = ca.create_channel(self.pvname, + callback=self.__on_connect) + self.chid = self._args['chid'] + ca.replace_access_rights_event(self.chid, + callback=self.__on_access_rights_event) + self.ftype = ca.promote_type(self.chid, + use_ctrl= self.form == 'ctrl', + use_time= self.form == 'time') + self._args['type'] = dbr.Name(self.ftype).lower() + + @_ensure_context + def force_connect(self, pvname=None, chid=None, conn=True, **kws): + if chid is None: chid = self.chid + if hasattr(chid, 'value'): + chid = chid.value + self._args['chid'] = self.chid = chid + self.__on_connect(pvname=pvname, chid=chid, conn=conn, **kws) + + @_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, + 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 + + acc = read_access + 2 * write_access + access_strs = ('no access', 'read-only', 'write-only', 'read/write') + self._args['access'] = access_strs[acc] + + for cb in self.access_callbacks: + 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 + if self.chid is None and chid is None: + ca.poll(5.e-4) + return + if conn: + ca.poll() + self.chid = self._args['chid'] = dbr.chid_t(chid) + try: + count = ca.element_count(self.chid) + except ca.ChannelAccessException: + time.sleep(0.025) + count = ca.element_count(self.chid) + self._args['nelm'] = count + + # allow reduction of elements, via count argument + 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) + self._check_auto_monitor() + + for conn_cb in self.connection_callbacks: + 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) + + # pv end of connect, force a read of access rights + self.force_read_access_rights() + + # waiting until the very end until to set self.connected prevents + # threads from thinking a connection is complete when it is actually + # still in progress. + self.connected = conn + + @_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. + ''' + count = self.count + chid = self.chid + + if count is None or chid 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""" + if not self.connected: + start_time = time.time() + if not self._conn_started: + self.connect(timeout=timeout) + + if not self.connected: + if timeout is None: + timeout = self.connection_timeout + while not self.connected and time.time()-start_time < timeout: + 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: + if timeout is None: + timeout = self.connection_timeout + ca.connect_channel(self.chid, timeout=timeout) + self._conn_started = True + return self.connected and self.ftype is not None + + @_ensure_context + def clear_auto_monitor(self): + """turn off auto-monitoring""" + self.auto_monitor = False + + def reconnect(self): + "try to reconnect PV" + 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) + + def get(self, count=None, as_string=False, as_numpy=True, + timeout=None, with_ctrlvars=False, use_monitor=True): + """returns current value of PV. Use the options: + 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. + + >>> get_pv('13BMD:m1.DIR').get() + 0 + >>> 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: + 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 + (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_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: + 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] + + 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] + + # 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 + complete, and optionally specifying a callback function to be run + when the processing is complete. + """ + if not self.wait_for_connection(): + return None + + if (self.ftype in (dbr.ENUM, dbr.TIME_ENUM, dbr.CTRL_ENUM) and + is_string(value)): + if self._args['enum_strs'] is None: + self.get_ctrlvars() + if value in self._args['enum_strs']: + # tuple.index() not supported in python2.5 + # value = self._args['enum_strs'].index(value) + for ival, val in enumerate(self._args['enum_strs']): + if val == value: + value = ival + break + + 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=_put_callback if use_complete or callback else None, + callback_data=callback_data) + + 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: + self._args['char_value'] = 'None' + return 'None' + ftype = self._args['ftype'] + ntype = ca.native_type(ftype) + if ntype == dbr.STRING: + self._args['char_value'] = val + return val + # char waveform as string + 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() + + if not isinstance(val, list): + # a scalar value from numpy, tolist() turns it into a + # native python integer + val = [val.tolist()] + else: + try: + # otherwise, try forcing it into a list. this will fail for + # scalar types + val = list(val) + except TypeError: + # and when it fails, make it a list of one scalar value + val = [val] + + if 0 in val: + firstnull = val.index(0) + else: + firstnull = len(val) + try: + cval = ''.join([chr(i) for i in val[:firstnull]]).rstrip() + except ValueError: + cval = '' + self._args['char_value'] = cval + return cval + + cval = repr(val) + if self.count > 1: + try: + length = len(val) + except TypeError: + length = 1 + cval = '' % (length, + dbr.Name(ftype).lower()) + elif ntype in (dbr.FLOAT, dbr.DOUBLE): + if call_ca and self._args['precision'] is None: + self.get_ctrlvars() + try: + prec = self._args['precision'] + fmt = "%%.%if" + if 4 < abs(int(log10(abs(val + 1.e-9)))): + fmt = "%%.%ig" + cval = (fmt % prec) % val + except (ValueError, TypeError, ArithmeticError): + cval = str(val) + elif ntype == dbr.ENUM: + if call_ca and self._args['enum_strs'] in ([], None): + self.get_ctrlvars() + try: + cval = self._args['enum_strs'][val] + except (TypeError, KeyError, IndexError): + cval = str(val) + + 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) + 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) + if kwds is not None: + self._args.update(kwds) + return kwds + + + def __on_changes(self, value=None, **kwd): + """internal callback function: do not overwrite!! + To have user-defined code run when the PV value changes, + use add_callback() + """ + self._args.update(kwd) + self._args['value'] = value + self._args['timestamp'] = kwd.get('timestamp', time.time()) + self._args['posixseconds'] = kwd.get('posixseconds', 0) + self._args['nanoseconds'] = kwd.get('nanoseconds', 0) + self._set_charval(self._args['value'], call_ca=False) + if self.verbose: + now = fmt_time(self._args['timestamp']) + ca.write('%s: %s (%s)'% (self.pvname, + self._args['char_value'], now)) + self.run_callbacks() + + @_ensure_context + def run_callbacks(self): + """run all user-defined callbacks with the current data + + Normally, this is to be run automatically on event, but + it is provided here as a separate function for testing + purposes. + """ + 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 + Note that callback functions are called with keyword/val + arguments including: + self._args (all PV data available, keys = __fields) + keyword args included in add_callback() + keyword 'cb_info' = (index, self) + where the 'cb_info' is provided as a hook so that a callback + function that fails may de-register itself (for example, if + a GUI resource is no longer available). + """ + try: + fcn, kwargs = self.callbacks[index] + except KeyError: + return + kwd = copy.copy(self._args) + kwd.update(kwargs) + kwd['cb_info'] = (index, self) + if callable(fcn): + fcn(**kwd) + + def add_callback(self, callback=None, index=None, run_now=False, + with_ctrlvars=True, **kw): + """add a callback to a PV. Optional keyword arguments + set here will be preserved and passed on to the callback + at runtime. + + 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 callable(callback): + if index is None: + index = 1 + if len(self.callbacks) > 0: + index = 1 + max(self.callbacks.keys()) + self.callbacks[index] = (callback, kw) + + if with_ctrlvars and self.connected: + self.get_ctrlvars() + if run_now: + self.get(as_string=True) + if self.connected: + 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): + "clear all callbacks" + self.callbacks.clear() + + def _getinfo(self): + "get information paragraph" + if not self.wait_for_connection(): + return None + self.get_ctrlvars() + out = [] + mod = 'native' + xtype = self._args['typefull'] + if '_' in xtype: + mod, xtype = xtype.split('_') + + fmt = '%i' + if xtype in ('float','double'): + fmt = '%g' + elif xtype in ('string','char'): + fmt = '%s' + + self._set_charval(self._args['value'], call_ca=False) + out.append("== %s (%s_%s) ==" % (self.pvname, mod, xtype)) + if self.count == 1: + val = self._args['value'] + out.append(' value = %s' % fmt % val) + else: + ext = {True:'...', False:''}[self.count > 10] + elems = range(min(5, self.count)) + try: + aval = [fmt % self._args['value'][i] for i in elems] + except TypeError: + aval = ('unknown',) + out.append(" value = array [%s%s]" % (",".join(aval), ext)) + for nam in ('char_value', 'count', 'nelm', 'type', 'units', + 'precision', 'host', 'access', + 'status', 'severity', 'timestamp', + 'posixseconds', 'nanoseconds', + 'upper_ctrl_limit', 'lower_ctrl_limit', + 'upper_disp_limit', 'lower_disp_limit', + 'upper_alarm_limit', 'lower_alarm_limit', + 'upper_warning_limit', 'lower_warning_limit'): + if hasattr(self, nam): + att = getattr(self, nam) + if att is not None: + if nam == 'timestamp': + att = "%.3f (%s)" % (att, fmt_time(att)) + elif nam == 'char_value': + att = "'%s'" % att + if len(nam) < 12: + out.append(' %.11s= %s' % (nam+' '*12, str(att))) + else: + out.append(' %.20s= %s' % (nam+' '*20, str(att))) + if xtype == 'enum': # list enum strings + out.append(' enum strings: ') + for index, nam in enumerate(self.enum_strs): + out.append(" %i = %s " % (index, nam)) + + if self._monref is not None: + msg = 'PV is internally monitored' + out.append(' %s, with %i user-defined callbacks:' % (msg, + len(self.callbacks))) + if len(self.callbacks) > 0: + for nam in sorted(self.callbacks.keys()): + cback = self.callbacks[nam][0] + 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('=============================') + return '\n'.join(out) + + def _getarg(self, arg): + "wrapper for property retrieval" + if self._args['value'] is None: + self.get() + if self._args[arg] is None: + if arg in ('status', 'severity', 'timestamp', + 'posixseconds', 'nanoseconds'): + self.get_timevars(timeout=1, warn=False) + else: + self.get_ctrlvars(timeout=1, warn=False) + return self._args.get(arg, None) + + def __getval__(self): + "get value" + return self._getarg('value') + + def __setval__(self, val): + "put-value" + return self.put(val) + + value = property(__getval__, __setval__, None, "value property") + + @property + def char_value(self): + "character string representation of value" + return self._getarg('char_value') + + @property + def status(self): + "pv status" + return self._getarg('status') + + @property + def type(self): + "pv type" + return self._args['type'] + + @property + def typefull(self): + "pv type" + return self._args['typefull'] + + @property + def host(self): + "pv host" + return self._getarg('host') + + @property + def count(self): + """count (number of elements). For array data and later EPICS versions, + this is equivalent to the .NORD field. See also 'nelm' property""" + if self._args['count'] is not None: + return self._args['count'] + else: + 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 + return ca.element_count(self.chid) + + @property + def read_access(self): + "read access" + return self._getarg('read_access') + + @property + def write_access(self): + "write access" + return self._getarg('write_access') + + @property + def access(self): + "read/write access as string" + return self._getarg('access') + + @property + def severity(self): + "pv severity" + return self._getarg('severity') + + @property + def timestamp(self): + "timestamp of last pv action" + return self._getarg('timestamp') + + @property + def posixseconds(self): + """integer seconds for timestamp of last pv action + using POSIX time convention""" + return self._getarg('posixseconds') + + @property + def nanoseconds(self): + "integer nanoseconds for timestamp of last pv action" + return self._getarg('nanoseconds') + + @property + def precision(self): + "number of digits after decimal point" + return self._getarg('precision') + + @property + def units(self): + "engineering units for pv" + return self._getarg('units') + + @property + def enum_strs(self): + "list of enumeration strings" + return self._getarg('enum_strs') + + @property + def upper_disp_limit(self): + "limit" + return self._getarg('upper_disp_limit') + + @property + def lower_disp_limit(self): + "limit" + return self._getarg('lower_disp_limit') + + @property + def upper_alarm_limit(self): + "limit" + return self._getarg('upper_alarm_limit') + + @property + def lower_alarm_limit(self): + "limit" + return self._getarg('lower_alarm_limit') + + @property + def lower_warning_limit(self): + "limit" + return self._getarg('lower_warning_limit') + + @property + def upper_warning_limit(self): + "limit" + return self._getarg('upper_warning_limit') + + @property + def upper_ctrl_limit(self): + "limit" + return self._getarg('upper_ctrl_limit') + + @property + def lower_ctrl_limit(self): + "limit" + return self._getarg('lower_ctrl_limit') + + @property + def info(self): + "info string" + return self._getinfo() + + @property + def put_complete(self): + "returns True if the last put-with-wait has completed" + return self._put_complete + + def __repr__(self): + "string representation" + + if self.connected: + if self.count == 1: + return self._fmtsca % self._args + else: + return self._fmtarr % self._args + else: + return "" % self.pvname + + def __eq__(self, other): + "test for equality" + try: + return (self.chid == other.chid) + except AttributeError: + return False + + @_ensure_context + def disconnect(self): + "disconnect PV" + self.connected = False + + ctx = ca.current_context() + pvid = (self.pvname, self.form, ctx) + if pvid in _PVcache_: + _PVcache_.pop(pvid) + + 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 + 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() + self._args = {}.fromkeys(self._fields) + ca.poll(evt=1.e-3, iot=1.0) + + def __del__(self): + if getattr(ca, 'libca', None) is None: + return + + try: + 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/qt/pvprobe_qt.py b/epics/qt/pvprobe_qt.py new file mode 100644 index 0000000..248df9d --- /dev/null +++ b/epics/qt/pvprobe_qt.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python + +import epics +import sys +try: + from PySide.QtGui import QWidget, QLabel, QLineEdit, QGridLayout, QApplication +except: + from PyQt4.QtGui import QWidget, QLabel, QLineEdit, QGridLayout, QApplication + +from epics.utils import BYTES2STR +class PVText(QLabel): + def __init__(self, pvname, **kws): + QLabel.__init__(self, '', **kws) + self.pv = None + self.cb_index = None + + def SetPV(self, pvname): + if self.pv is not None and self.cb_index is not None: + self.pv.remove_callback(self.cb_index) + + self.pv = epics.PV(BYTES2STR(pvname)) + self.setText(self.pv.get(as_string=True)) + self.cb_index = self.pv.add_callback(self.onPVChange) + + def onPVChange(self, pvname=None, char_value=None, **kws): + self.setText(char_value) + +class PVLineEdit(QLineEdit): + def __init__(self, pvname=None, **kws): + QLineEdit.__init__(self, **kws) + self.returnPressed.connect(self.onReturn) + self.pv = None + self.cb_index = None + if pvname is not None: + self.SetPV(pvname) + + def SetPV(self, pvname): + if self.pv is not None and self.cb_index is not None: + self.pv.remove_callback(self.cb_index) + + self.pv = epics.PV(BYTES2STR(pvname)) + self.cb_index = self.pv.add_callback(self.onPVChange) + + def onPVChange(self, pvname=None, char_value=None, **kws): + self.setText(char_value) + + def onReturn(self): + self.pv.put(BYTES2STR(self.text())) + +class PVProbe(QWidget): + def __init__(self, parent=None): + QWidget.__init__(self, parent) + self.setWindowTitle("PyQt4 PV Probe:") + + self.pv1name = QLineEdit() + self.pv2name = QLineEdit() + self.value = PVText(None) + self.pvedit = PVLineEdit() + + grid = QGridLayout() + grid.addWidget(QLabel("PV1 Name (Read-only):"), 0, 0) + grid.addWidget(QLabel("PV1 Value (Read-only):"), 1, 0) + grid.addWidget(QLabel("PV2 Name: (Read-write):"), 2, 0) + grid.addWidget(QLabel("PV2 Value (Read-write):"), 3, 0) + grid.addWidget(self.pv1name, 0, 1) + grid.addWidget(self.value, 1, 1) + grid.addWidget(self.pv2name, 2, 1) + grid.addWidget(self.pvedit, 3, 1) + + self.pv1name.returnPressed.connect(self.onPV1NameReturn) + self.pv2name.returnPressed.connect(self.onPV2NameReturn) + + self.setLayout(grid) + + def onPV1NameReturn(self): + self.value.SetPV(self.pv1name.text()) + + def onPV2NameReturn(self): + self.pvedit.SetPV(self.pv2name.text()) + +if __name__ == '__main__': + app = QApplication(sys.argv) + probe = PVProbe() + probe.show() + sys.exit(app.exec_()) diff --git a/epics/utils.py b/epics/utils.py new file mode 100644 index 0000000..263f9ca --- /dev/null +++ b/epics/utils.py @@ -0,0 +1,74 @@ +""" +String and data utils, where implementation differs between Python 2 & 3 +""" +import sys +from copy import deepcopy +import os + +PY_MAJOR, PY_MINOR = sys.version_info[:2] + +if PY_MAJOR >= 3: + from . import utils3 as utils_mod +else: + from . import utils2 as utils_mod + +STR2BYTES = utils_mod.STR2BYTES +BYTES2STR = utils_mod.BYTES2STR +NULLCHAR = utils_mod.NULLCHAR +NULLCHAR_2 = utils_mod.NULLCHAR_2 +strjoin = utils_mod.strjoin +is_string = utils_mod.is_string +is_string_or_bytes = utils_mod.is_string_or_bytes +ascii_string = utils_mod.ascii_string + +memcopy = deepcopy +if PY_MAJOR == 2 and PY_MINOR == 5: + def memcopy(a): + return a + +def clib_search_path(lib): + '''Assemble path to c library. + + Parameters + ---------- + lib : str + Either 'ca' or 'Com'. + + Returns + -------- + str : string + + Examples + -------- + >>> clib_search_path('ca') + 'linux64/libca.so' + + ''' + + # determine which libca / libCom dll is appropriate + 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': + libsrc = 'win' + libfmt = '%s.dll' + elif sys.platform == 'darwin': + libsrc = 'darwin' + libfmt = 'lib%s.dylib' + elif sys.platform.startswith('linux'): + libsrc = 'linux' + + else: + return None + + return os.path.join("%s%s" % (libsrc, nbits), libfmt % lib) diff --git a/epics/utils2.py b/epics/utils2.py new file mode 100644 index 0000000..20cd860 --- /dev/null +++ b/epics/utils2.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python +""" +String Utils for Python 2 +""" +import sys +if sys.version_info[0] != 2: + raise ImportError(" Python version 2 required") + +NULLCHAR_2 = '\x00' +NULLCHAR = '\x00' +STR2BYTES = str +BYTES2STR = str + +def strjoin(sep, seq): + "join string sequence with a separator" + return sep.join(seq) + +def is_string(s): + return isinstance(s, basestring) + +is_string_or_bytes = is_string + +ascii_string = str +# def ascii_string(s): +# if isinstance(s, unicode): +# return str(s) +# else: +# return s diff --git a/epics/utils3.py b/epics/utils3.py new file mode 100644 index 0000000..4ae9ee9 --- /dev/null +++ b/epics/utils3.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python +""" +String Utils for Python 3 +""" +import os +import sys +if sys.version_info[0] != 3: + raise ImportError(" Python version 3 required") + +EPICS_STR_ENCODING = os.environ.get('PYTHONIOENCODING', 'utf-8') +NULLCHAR_2 = '\x00' +NULLCHAR = b'\x00' + +def s2b(st1): + 'string to byte conversion' + if isinstance(st1, bytes): + return st1 + return bytes(st1, EPICS_STR_ENCODING) + +def b2s(st1): + 'byte to string conversion' + if isinstance(st1, str): + return st1 + elif isinstance(st1, bytes): + return str(st1, EPICS_STR_ENCODING) + else: + return str(st1) + +STR2BYTES, BYTES2STR = s2b, b2s + +def strjoin(sep, seq): + "join string sequence with a separator" + if isinstance(sep, bytes): + sep = BYTES2STR(sep) + if len(seq) == 0: + seq = '' + elif isinstance(seq[0], bytes): + tmp =[] + for i in seq: + if i == NULLCHAR: + break + tmp.append(BYTES2STR(i)) + seq = tmp + return sep.join(seq) + +def is_string(s): + return isinstance(s, str) + +def is_string_or_bytes(s): + return isinstance(s, str) or isinstance(s, bytes) + +def ascii_string(s): + return bytes(str(s), EPICS_STR_ENCODING) diff --git a/epics/wx/__init__.py b/epics/wx/__init__.py new file mode 100755 index 0000000..61a22a8 --- /dev/null +++ b/epics/wx/__init__.py @@ -0,0 +1,44 @@ +""" +This module provides wxPython widgets specially designed to work as +Epics Controls. In general, these controls combine a wx widget with +an Epics PV, and allow automatic updating of the widget when the +associated PV changes. +""" +from . import motorpanel, motordetailframe, wxlib, ogllib, utils + +MotorPanel = motorpanel.MotorPanel +MotorDetailPanel = motordetailframe.MotorDetailPanel +MotorDetailFrame = motordetailframe.MotorDetailFrame + +PVText = pvText = wxlib.PVText +PVAlarm = pvAlarm = wxlib.PVAlarm +PVFloatCtrl = pvFloatCtrl = wxlib.PVFloatCtrl +PVTextCtrl = pvTextCtrl = wxlib.PVTextCtrl +PVStaticText = pvStaticText = wxlib.PVStaticText +PVEnumButtons = pvEnumButtons = wxlib.PVEnumButtons +PVEnumChoice = pvEnumChoice = wxlib.PVEnumChoice +PVBitmap = pvBitmap = wxlib.PVBitmap +PVCheckBox = pvCheckBox = wxlib.PVCheckBox +PVFloatSpin = pvFloatSpin = wxlib.PVFloatSpin +PVSpinCtrl = pvSpinCtrl = wxlib.PVSpinCtrl +PVButton = pvButton = wxlib.PVButton +PVRadioButton = pvRadioButton = wxlib.PVRadioButton +PVComboBox = pvComboBox = wxlib.PVComboBox +PVEnumComboBox = pvEnumComboBox = wxlib.PVEnumComboBox +PVCollapsiblePane = pvCollapsiblePane = wxlib.PVCollapsiblePane + +# OGL shapes +PVRectangle = pvRectangle = ogllib.PVRectangle +PVCircle = pvCircle = ogllib.PVCircle + +set_sizer = utils.set_sizer +set_float = utils.set_float + +Closure = utils.Closure +FloatCtrl = utils.FloatCtrl + +DelayedEpicsCallback = wxlib.DelayedEpicsCallback +EpicsFunction = wxlib.EpicsFunction +finalize_epics = wxlib.finalize_epics +EpicsTimer = wxlib.EpicsTimer + diff --git a/epics/wx/motordetailframe.py b/epics/wx/motordetailframe.py new file mode 100755 index 0000000..4bd519b --- /dev/null +++ b/epics/wx/motordetailframe.py @@ -0,0 +1,429 @@ +""" +wxFrame for Detailed Motor Settings, ala medm More (+Setup) screen +""" + +import time +import wx +from wx.lib.scrolledpanel import ScrolledPanel + +from epics.wx.wxlib import (PVText, PVFloatCtrl, PVTextCtrl, + PVEnumButtons, PVEnumChoice, + DelayedEpicsCallback, EpicsFunction) + + +from .utils import set_sizer, LCEN, RCEN, CEN, FileSave + +TMPL_TOP = '''file "$(CARS)/CARSApp/Db/motor.db" +{ +pattern +{P, M, DTYP, C, S, DESC, EGU, DIR, VELO, VBAS, ACCL, BDST,BVEL,BACC, SREV,UREV,PREC,DHLM,DLLM} +''' + +def xLabel(parent, label): + "simple label" + return wx.StaticText(parent, label=" %s" % label, style=wx.ALIGN_BOTTOM) + +def xTitle(parent, label, fontsize=13, color='Blue'): + "simple title" + wid = wx.StaticText(parent, label=" %s" % label, style=wx.ALIGN_BOTTOM) + font = wid.GetFont() + font.PointSize = fontsize + wid.SetFont(font) + wid.SetForegroundColour(color) + return wid + +MAINSIZE = (525, 750) +class MotorDetailFrame(wx.Frame): + """ Detailed Motor Setup 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, + style=wx.DEFAULT_FRAME_STYLE|wx.TAB_TRAVERSAL) + + self.motor = motor + devtype = motor.get('DTYP', as_string=True) + motor_pvname = self.motor._prefix + if motor_pvname.endswith('.'): + motor_pvname = motor_pvname[:-1] + + self.SetTitle("Motor Details: %s | %s | (%s)" % (motor_pvname, + self.motor.DESC, + devtype)) + + sizer = wx.BoxSizer(wx.VERTICAL) + panel = MotorDetailPanel(parent=self, motor=motor) + + sizer.Add(panel, 1, wx.EXPAND) + + # self.createMenu() + set_sizer(self, sizer) + + self.Show() + self.Raise() + + def createMenu(self, event=None): + fmenu = wx.Menu() + id_save = wx.NewId() + id_copy = wx.NewId() + fmenu.Append(id_save, "&Save Template File", + "Save Motor Template for this motor") + fmenu.Append(id_copy, "&Copy Template" + "Copy Motor Template to Clipboard") + + menuBar = wx.MenuBar() + menuBar.Append(fmenu, "&File"); + + self.SetMenuBar(menuBar) + self.Bind(wx.EVT_MENU, self._onSaveTemplate, id=id_save) + self.Bind(wx.EVT_MENU, self._onCopyTemplate, id=id_copy) + + @EpicsFunction + def MakeTemplate(self, event=None): + out = TMPL_TOP + return out + + @EpicsFunction + def _onSaveTemplate(self, event=None): + name = self.motor.pvname + fname = FileSave(self, 'Save Template File', + wildcard='INI (*.template)|*.template|All files (*.*)|*.*', + default_file='Motor_%s.template' % name) + if fname is not None: + fout = open(fname, 'w+') + fout.write("%s\n" % self.MakeTemplate()) + fout.close() + + @EpicsFunction + def _onCopyTemplate(self, event=None): + dat = wx.TextDataObject() + dat.SetText(self.MakeTemplate()) + 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) + + self.Freeze() + self.motor = motor + prec = motor.PREC + + sizer = wx.BoxSizer(wx.VERTICAL) + + + ds = wx.GridBagSizer(1, 6) + dp = wx.Panel(self) + + ds.Add(xLabel(dp, 'Label'), (0, 0), (1, 1), LCEN, 5) + 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)), + (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) + sizer.Add(dp, 0) + + sizer.Add((3, 3), 0) + sizer.Add(wx.StaticLine(self, size=(100, 2)), 0, wx.EXPAND) + sizer.Add((3, 3), 0) + + ds = wx.GridBagSizer(6, 4) + dp = wx.Panel(self) + nrow = 0 + ds.Add(xTitle(dp,"Drive"), (nrow, 0), (1, 1), LCEN, 5) + ds.Add(xLabel(dp,"User" ), (nrow, 1), (1, 1), CEN, 5) + ds.Add(xLabel(dp,"Dial" ), (nrow, 2), (1, 1), CEN, 5) + ds.Add(xLabel(dp,"Raw" ), (nrow, 3), (1, 1), CEN, 5) + + #### + nrow += 1 + self.info = wx.StaticText(dp, label='', size=(55, 20), style=CEN) + self.info.SetForegroundColour("Red") + + ds.Add(xLabel(dp,"High Limit"), (nrow, 0), (1, 1), LCEN, 5) + ds.Add(self.MotorCtrl(dp,'HLM'), (nrow, 1), (1, 1), CEN, 5) + ds.Add(self.MotorCtrl(dp,'DHLM'), (nrow, 2), (1, 1), CEN, 5) + ds.Add(self.info, (nrow, 3), (1, 1), CEN, 5) + + #### + nrow += 1 + ostyle = RCEN|wx.EXPAND + ds.Add(xLabel(dp,"Readback"), (nrow, 0), (1, 1), LCEN, 5) + ds.Add(self.MotorText(dp, 'RBV'), (nrow, 1), (1, 1), ostyle, 5) + ds.Add(self.MotorText(dp, 'DRBV'), (nrow, 2), (1, 1), ostyle, 5) + ds.Add(self.MotorText(dp, 'RRBV'), (nrow, 3), (1, 1), ostyle, 5) + + #### + nrow += 1 + self.drives = [self.MotorCtrl(dp, 'VAL'), + self.MotorCtrl(dp, 'DVAL'), + self.MotorCtrl(dp, 'RVAL')] + + ds.Add(xLabel(dp,"Move"), (nrow, 0), (1, 1), LCEN, 5) + ds.Add(self.drives[0], (nrow, 1), (1, 1), CEN, 5) + ds.Add(self.drives[1], (nrow, 2), (1, 1), CEN, 5) + ds.Add(self.drives[2], (nrow, 3), (1, 1), CEN, 5) + + nrow += 1 + ds.Add(xLabel(dp,"Low Limit"), (nrow, 0), (1, 1), LCEN, 5) + ds.Add(self.MotorCtrl(dp, 'LLM'), (nrow, 1), (1, 1), CEN, 5) + ds.Add(self.MotorCtrl(dp, 'DLLM'), (nrow, 2), (1, 1), CEN, 5) + + #### + + twk_sizer = wx.BoxSizer(wx.HORIZONTAL) + twk_panel = wx.Panel(dp) + twk_val = PVFloatCtrl(twk_panel, size=(110, -1), precision=prec) + twk_val.SetPV(self.motor.PV('TWV')) + + twk_left = wx.Button(twk_panel, label='<', size=(30, 30)) + twk_right = wx.Button(twk_panel, label='>', size=(30, 30)) + twk_left.Bind(wx.EVT_BUTTON, self.OnLeftButton) + twk_right.Bind(wx.EVT_BUTTON, self.OnRightButton) + twk_sizer.AddMany([(twk_left, 0, CEN), + (twk_val, 0, CEN), + (twk_right, 0, CEN)]) + + set_sizer(twk_panel, twk_sizer) + + nrow += 1 + ds.Add(xLabel(dp,"Tweak"), (nrow, 0), (1, 1), LCEN, 5) + ds.Add(twk_panel, (nrow, 1), (1, 2), wx.ALIGN_LEFT, 5) + + epv = self.motor.PV('disabled') + + able_btns = PVEnumButtons(dp, pv=epv, orientation = wx.VERTICAL, + size=(80, 60)) + + ds.Add(able_btns, (nrow-1, 3), (2, 1), CEN, 5) + + stop_btns = PVEnumButtons(dp, pv=self.motor.PV('SPMG'), + orientation = wx.VERTICAL, + size=(100, 125)) + + ds.Add(stop_btns, (2, 4), (4, 1), wx.ALIGN_RIGHT, 5) + + for attr in ('LLM', 'HLM', 'DLLM', 'DHLM'): + pv = self.motor.PV(attr) + pv.add_callback(self.OnLimitChange, wid=self.GetId(), attr=attr) + + # + set_sizer(dp, ds) # ,fit=True) + sizer.Add(dp, 0) + + #### + sizer.Add((3, 3), 0) + sizer.Add(wx.StaticLine(self, size=(100, 2)), 0, wx.EXPAND) + sizer.Add((3, 3), 0) + sizer.Add(xTitle(self, 'Calibration'), 0, LCEN, 25) + + ds = wx.GridBagSizer(6, 5) + dp = wx.Panel(self) + + ds.Add(xLabel(dp, 'Mode: '), (0, 0), (1, 1), LCEN, 5) + + ds.Add(PVEnumButtons(dp, pv=self.motor.PV('SET'), + orientation = wx.HORIZONTAL, + size=(175, 25)), (0, 1), (1, 2), wx.ALIGN_LEFT) + + ds.Add(xLabel(dp, 'Direction: '), (1, 0), (1, 1), LCEN, 5) + ds.Add(PVEnumButtons(dp, pv=self.motor.PV('DIR'), + orientation=wx.HORIZONTAL, + size=(175, 25)), (1, 1), (1, 2), wx.ALIGN_LEFT) + + ds.Add(xLabel(dp, 'Freeze Offset: '), (0, 4), (1, 1), LCEN, 5) + ds.Add(PVEnumChoice(dp, pv=self.motor.PV('FOFF'), + size=(110, -1)), (0, 5), (1, 1), CEN) + + ds.Add(xLabel(dp, 'Offset Value: '), (1, 4), (1, 1), LCEN, 5) + ds.Add(self.MotorCtrl(dp,'OFF'), (1, 5), (1, 1), CEN) + + set_sizer(dp, ds) + sizer.Add(dp, 0) + ##### + + sizer.Add((3, 3), 0) + sizer.Add(wx.StaticLine(self, size=(100, 2)), 0, wx.EXPAND) + sizer.Add((3, 3), 0) + # + ds = wx.GridBagSizer(6, 3) + dp = wx.Panel(self) + nrow = 0 + + ds.Add(xTitle(dp, "Dynamics"), (nrow, 0), (1, 1), LCEN, 55) + ds.Add(xLabel(dp, "Normal" ), (nrow, 1), (1, 1), CEN) + ds.Add(xLabel(dp, "Backlash" ), (nrow, 2), (1, 1), CEN) + + #### + nrow += 1 + ds.Add(xLabel(dp, "Max Speed"), (nrow, 0), (1, 1), LCEN, 5) + ds.Add(self.MotorCtrl(dp, 'VMAX'), (nrow, 1), (1, 1), CEN) + + nrow += 1 + ds.Add(xLabel(dp, "Speed"), (nrow, 0), (1, 1), LCEN, 5) + ds.Add(self.MotorCtrl(dp, 'VELO'), (nrow, 1), (1, 1), CEN) + ds.Add(self.MotorCtrl(dp, 'BVEL'), (nrow, 2), (1, 1), CEN) + + nrow += 1 + ds.Add(xLabel(dp, "Base Speed"), (nrow, 0), (1, 1), LCEN, 5) + ds.Add(self.MotorCtrl(dp, 'VBAS'), (nrow, 1), (1, 1), CEN) + + nrow += 1 + ds.Add(xLabel(dp, "Accel (s)"), (nrow, 0), (1, 1), LCEN, 5) + ds.Add(self.MotorCtrl(dp, 'ACCL'), (nrow, 1), (1, 1), CEN) + ds.Add(self.MotorCtrl(dp, 'BACC'), (nrow, 2), (1, 1), CEN) + + nrow += 1 + ds.Add(xLabel(dp, "Backslash Distance"), (nrow, 0), (1, 2), LCEN, 5) + ds.Add(self.MotorCtrl(dp, 'BDST'), (nrow, 2), (1, 1), CEN) + + nrow += 1 + ds.Add(xLabel(dp, "Move Fraction"), (nrow, 0), (1, 2), LCEN, 5) + ds.Add(self.MotorCtrl(dp, 'FRAC'), (nrow, 2), (1, 1), CEN) + + set_sizer(dp, ds) # ,fit=True) + + sizer.Add(dp, 0) + + sizer.Add((3, 3), 0) + sizer.Add(wx.StaticLine(self, size=(100, 2)), 0, wx.EXPAND) + sizer.Add((3, 3), 0) + sizer.Add(xTitle(self, 'Resolution, Readback, and Retries'), 0, LCEN, 5) + + ds = wx.GridBagSizer(4, 4) + dp = wx.Panel(self) + nrow = 0 + + ds.Add(xLabel(dp, "Motor Res"), (nrow, 0), (1, 1), LCEN, 5) + ds.Add(self.MotorCtrl(dp, 'MRES'), (nrow, 1), (1, 1), CEN) + ds.Add(xLabel(dp, "Encoder Res"), (nrow, 2), (1, 1), LCEN, 5) + ds.Add(self.MotorCtrl(dp, 'ERES'), (nrow, 3), (1, 1), CEN) + + nrow += 1 + ds.Add(xLabel(dp, "Steps / Rev"), (nrow, 0), (1, 1), LCEN, 5) + ds.Add(self.MotorCtrl(dp, 'SREV'), (nrow, 1), (1, 1), CEN) + ds.Add(xLabel(dp, "Units / Rev"), (nrow, 2), (1, 1), LCEN, 5) + ds.Add(self.MotorCtrl(dp, 'UREV'), (nrow, 3), (1, 1), CEN) + + + nrow += 1 + ds.Add(xLabel(dp, "Readback Res"), (nrow, 0), (1, 1), LCEN, 5) + ds.Add(self.MotorCtrl(dp, 'RRES'), (nrow, 1), (1, 1), CEN) + ds.Add(xLabel(dp, "Readback Delay (s)"), (nrow, 2), (1, 1), LCEN, 5) + ds.Add(self.MotorCtrl(dp, 'DLY'), (nrow, 3), (1, 1), CEN) + + nrow += 1 + ds.Add(xLabel(dp, "Retry Deadband"), (nrow, 0), (1, 1), LCEN, 5) + ds.Add(self.MotorCtrl(dp, 'RDBD'), (nrow, 1), (1, 1), CEN) + ds.Add(xLabel(dp, "Max Retries"), (nrow, 2), (1, 1), LCEN, 5) + ds.Add(self.MotorCtrl(dp, 'RTRY'), (nrow, 3), (1, 1), CEN) + + nrow += 1 + ds.Add(xLabel(dp, "Use Encoder"), (nrow, 0), (1, 1), LCEN, 5) + ds.Add(PVEnumChoice(dp, pv=self.motor.PV('UEIP'), + size=(110, -1)), (nrow, 1), (1, 1), CEN) + ds.Add(xLabel(dp, "Use Readback"), (nrow, 2), (1, 1), LCEN, 5) + ds.Add(PVEnumChoice(dp, pv=self.motor.PV('URIP'), + size=(110, -1)), (nrow, 3), (1, 1), CEN) + + nrow += 1 + ds.Add(xLabel(dp, "Use NTM"), (nrow, 0), (1, 1), LCEN, 5) + ds.Add(PVEnumChoice(dp, pv=self.motor.PV('NTM'), + size=(110, -1)), (nrow, 1), (1, 1), CEN) + ds.Add(xLabel(dp, "NTM Factor"), (nrow, 2), (1, 1), LCEN, 5) + ds.Add(self.MotorCtrl(dp, 'NTMF'), (nrow, 3), (1, 1), CEN) + + + set_sizer(dp, ds) + sizer.Add(dp, 0) + sizer.Add(wx.StaticLine(self, size=(100, 2)), 0, wx.EXPAND) + + + for attr in self.__motor_fields: + self.motor.PV(attr).add_callback(self.OnMotorEvent, + wid=self.GetId(), field=attr) + + self.info.SetLabel('') + for f in ('HLS', 'LLS', 'LVIO', 'SET'): + if self.motor.get(f): + wx.CallAfter(self.OnMotorEvent, + pvname=self.motor.PV(f).pvname, field=f) + + set_sizer(self, sizer, fit=True) + self.SetupScrolling() + self.Thaw() + + @DelayedEpicsCallback + def OnMotorEvent(self, pvname=None, field=None, **kws): + "Motor event handler" + if pvname is None: + return None + + field_val = self.motor.get(field) + if field in ('LVIO', 'HLS', 'LLS'): + s = '' + if field_val != 0: + s = 'Limit!' + self.info.SetLabel(s) + + elif field == 'SET': + color = 'Yellow' + if field_val == 0: + color = 'White' + for d in self.drives: + d.SetBackgroundColour(color) + d.Refresh() + + def MotorCtrl(self, panel, attr, size=(80, -1)): + "PVFloatCtrl for a Motor attribute" + return PVFloatCtrl(panel, size=size, + precision= self.motor.PREC, + pv=self.motor.PV(attr), + style = wx.TE_RIGHT) + + def MotorText(self, panel, attr, size=(80, -1)): + "PVText for a Motor attribute" + pv = self.motor.PV(attr) + return PVText(panel, pv=pv, as_string=True, + size=size, style=wx.ALIGN_CENTER|wx.CENTER) + + def MotorTextCtrl(self, panel, attr, size=(80, -1)): + "PVTextCtrl for a Motor attribute" + pv = self.motor.PV(attr) + return PVTextCtrl(panel, pv=pv, size=size, + style=wx.ALIGN_LEFT|wx.TE_PROCESS_ENTER) + + @DelayedEpicsCallback + def OnLimitChange(self, attr=None, value=None, **kws): + "limit-change callback" + funcs = {'low_limit': self.drives[0].SetMin, + 'high_limit': self.drives[0].SetMax, + 'dial_low_limit': self.drives[1].SetMin, + 'dial_high_limit': self.drives[1].SetMax} + if attr in funcs: + funcs[attr](value) + + @EpicsFunction + def OnLeftButton(self, event=None): + "left button event handler" + if self.motor is not None: + self.motor.tweak(direction='reverse') + event.Skip() + + @EpicsFunction + def OnRightButton(self, event=None): + "right button event handler" + if self.motor is not None: + self.motor.tweak(direction='forward') + event.Skip() diff --git a/epics/wx/motorpanel.py b/epics/wx/motorpanel.py new file mode 100755 index 0000000..ecceef0 --- /dev/null +++ b/epics/wx/motorpanel.py @@ -0,0 +1,312 @@ +#!/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 six +import wx +try: + from wx._core import PyDeadObjectError +except: + PyDeadObjectError = Exception + +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 + +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 callable(messenger): + 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 + + epics.poll() + try: + if self.motor is not None: + for i in self.__motor_fields: + self.motor.clear_callback(attr=i) + except PyDeadObjectError: + return + + if isinstance(motor, six.string_types): + self.motor = epics.Motor(motor) + elif isinstance(motor, epics.Motor): + self.motor = motor + self.motor.get_info() + + + if self.format is None: + self.format = "%%.%if" % self.motor.PREC + self.FillPanel() + for attr in self.__motor_fields: + self.motor.get_pv(attr).add_callback(self.OnMotorEvent, + wid=self.GetId(), + field=attr) + if self._size == 'full': + self.SetTweak(self.format % self.motor.TWV) + + @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) + 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/epics/wx/ogllib.py b/epics/wx/ogllib.py new file mode 100644 index 0000000..ffa877a --- /dev/null +++ b/epics/wx/ogllib.py @@ -0,0 +1,110 @@ +""" +wx OGL (2d graphics library) utility functions for Epics and wxPython +interaction + +OGL is a (somewhat old-fashioned) 2D drawing library included with wxPython. +There are probably newer/better drawing libraries, but OGL works quite well +for drawing simple shapes or bitmaps. + +""" +import wx.lib.ogl as ogl +from .wxlib import PVMixin + +class PVShapeMixin(PVMixin): + """ + Mixin for any Shape that has PV callback support + + """ + 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 + when the PV value changes. The brush is used to paint the shape foreground + + The argument should be a dictionary with keys as PV values (string if available), and values + as wx.Brush instances. + + """ + self.brushTranslations = translations + + def SetPenTranslations(self, translations): + """ + Set a dictionary of value->bpen translations that will be set automatically + when the PV value changes. The pen is used to paint the shape outline. + + The argument should be a dictionary with keys as PV values (string if available), and values + as wx.Brush instances. + + """ + self.penTranslations = translations + + + def SetShownTranslations(self, translations): + """ + Set a dictionary of value->boolean 'Shown' translations that will be set automatically + when the PV value changes. The value is used to show/hide the shape. + + """ + self.shownTranslations = translations + + + 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]) + if raw_value in self.penTranslations: + self.SetPen(self.penTranslations[raw_value]) + if raw_value in self.shownTranslations: + self.Show(self.shownTranslations[raw_value]) + self.PVChanged(raw_value) + self.Invalidate() + + def PVChanged(self, raw_value): + """ + Override this method if you want your shape to do any special processing when the + PV changes + + Note that the shape will be automatically invalidated (redrawn) after this method is called. + + """ + pass + + + def Invalidate(self): + """ + Invalidate the shape's area on the parent shape canvas to cause a redraw + (convenience method) + + """ + (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) + +class PVCircle(ogl.CircleShape, PVShapeMixin): + """ + A CircleShape which is associated with a particular PV value + + """ + def __init__(self, diameter, pv=None, pvname=None): + ogl.CircleShape.__init__(self, diameter) + PVShapeMixin.__init__(self, pv, pvname) diff --git a/epics/wx/ordereddict.py b/epics/wx/ordereddict.py new file mode 100644 index 0000000..72f8850 --- /dev/null +++ b/epics/wx/ordereddict.py @@ -0,0 +1,125 @@ +# Copyright (c) 2009 Raymond Hettinger +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + +from UserDict import DictMixin +class OrderedDict(dict, DictMixin): + def __init__(self, *args, **kwds): + if len(args) > 1: + raise TypeError('expected at most 1 arguments, got %d' % len(args)) + try: + self.__end + except AttributeError: + self.clear() + self.update(*args, **kwds) + + def clear(self): + self.__end = end = [] + end += [None, end, end] # sentinel node for doubly linked list + self.__map = {} # key --> [key, prev, next] + dict.clear(self) + + def __setitem__(self, key, value): + if key not in self: + end = self.__end + curr = end[1] + curr[2] = end[1] = self.__map[key] = [key, curr, end] + dict.__setitem__(self, key, value) + + def __delitem__(self, key): + dict.__delitem__(self, key) + key, prev, next = self.__map.pop(key) + prev[2] = next + next[1] = prev + + def __iter__(self): + end = self.__end + curr = end[2] + while curr is not end: + yield curr[0] + curr = curr[2] + + def __reversed__(self): + end = self.__end + curr = end[1] + while curr is not end: + yield curr[0] + curr = curr[1] + + def popitem(self, last=True): + if not self: + raise KeyError('dictionary is empty') + if last: + key = reversed(self).next() + else: + key = iter(self).next() + value = self.pop(key) + return key, value + + def __reduce__(self): + items = [[k, self[k]] for k in self] + tmp = self.__map, self.__end + del self.__map, self.__end + inst_dict = vars(self).copy() + self.__map, self.__end = tmp + if inst_dict: + return (self.__class__, (items,), inst_dict) + return self.__class__, (items,) + + def keys(self): + return list(self) + + setdefault = DictMixin.setdefault + update = DictMixin.update + pop = DictMixin.pop + values = DictMixin.values + items = DictMixin.items + iterkeys = DictMixin.iterkeys + itervalues = DictMixin.itervalues + iteritems = DictMixin.iteritems + + def __repr__(self): + if not self: + return '%s()' % (self.__class__.__name__,) + return '%s(%r)' % (self.__class__.__name__, self.items()) + + def copy(self): + return self.__class__(self) + + @classmethod + def fromkeys(cls, iterable, value=None): + d = cls() + for key in iterable: + d[key] = value + return d + + def __eq__(self, other): + if isinstance(other, OrderedDict): + if len(self) != len(other): + return False + for p, q in zip(self.items(), other.items()): + if p != q: + return False + return True + return dict.__eq__(self, other) + + def __ne__(self, other): + return not self == other diff --git a/epics/wx/utils.py b/epics/wx/utils.py new file mode 100755 index 0000000..3185123 --- /dev/null +++ b/epics/wx/utils.py @@ -0,0 +1,513 @@ +#!/usr/bin/env python +# epics/wx/utils.py +""" +This is a collection of general purpose utility functions and classes, +especially useful for wx functionality +""" +import wx +import wx.lib.masked as masked + +import os +import array +import six +if six.PY3: + maketrans = str.maketrans +else: + from string import maketrans + +BAD_FILECHARS = ';~,`!%$@$&^?*#:"/|\'\\\t\r\n (){}[]<>' +GOOD_FILECHARS = '_'*len(BAD_FILECHARS) +TRANS_FILE = maketrans(BAD_FILECHARS, GOOD_FILECHARS) + +HAS_NUMPY = False +try: + import numpy + HAS_NUMPY = True +except ImportError: + pass + +# some common abbrevs for wx ALIGNMENT styles +RIGHT = wx.ALIGN_RIGHT +LEFT = wx.ALIGN_LEFT +CEN = wx.ALIGN_CENTER +LCEN = wx.ALIGN_CENTER_VERTICAL|wx.ALIGN_LEFT +RCEN = wx.ALIGN_CENTER_VERTICAL|wx.ALIGN_RIGHT +CCEN = wx.ALIGN_CENTER_VERTICAL|wx.ALIGN_CENTER +LTEXT = wx.ST_NO_AUTORESIZE|wx.ALIGN_CENTER + + +def make_steps(prec=3, tmin=0, tmax=10, base=10, steps=(1, 2, 5)): + """make a list of 'steps' to use for a numeric ComboBox + returns a list of floats, such as + [0.01, 0.02, 0.05, 0.10, 0.20, 0.50, 1.00, 2.00...] + """ + steplist = [] + power = -prec + step = tmin + while True: + decade = base**power + for step in (j*decade for j in steps): + if step > 0.99*tmin and step <= tmax and step not in steplist: + steplist.append(step) + if step >= tmax: + break + power += 1 + return steplist + +def set_sizer(panel, sizer=None, style=wx.VERTICAL, fit=False): + """ utility for setting wx Sizer """ + if sizer is None: + sizer = wx.BoxSizer(style) + panel.SetAutoLayout(1) + panel.SetSizer(sizer) + if fit: + sizer.Fit(panel) + +def set_float(val): + """ utility to set a floating value, + useful for converting from strings """ + out = None + if not val in (None, ''): + try: + out = float(val) + except ValueError: + return None + if HAS_NUMPY: + if numpy.isnan(out): + out = default + else: + if not(out > 0) and not(out<0) and not(out==0): + out = default + return out + +def pack(window, sizer): + "simple wxPython pack function" + window.SetSizer(sizer) + sizer.Fit(window) + +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 callable(action): + thisb.Bind(wx.EVT_BUTTON, action) + return thisb + +def add_menu(parent, menu, label='', text='', action=None): + "add submenu" + wid = wx.NewId() + menu.Append(wid, label, text) + if callable(action): + parent.Bind(wx.EVT_MENU, action, id=wid) + +def popup(parent, message, title, style=None): + """ + generic popup message dialog, returns + output of MessageDialog.ShowModal() + """ + if style is None: + style = wx.OK|wx.ICON_INFORMATION + dlg = wx.MessageDialog(parent, message, title, style) + ret = dlg.ShowModal() + dlg.Destroy() + return ret + +def empty_bitmap(width, height, value=255): + """return empty wx.BitMap""" + data = array.array('B', [value]*3*width*height) + return wx.BitmapFromBuffer(width, height, data) + +def fix_filename(fname): + """ + fix string to be a 'good' filename. This may be a more + restrictive than the OS, but avoids nasty cases. + """ + out = str(s).translate(TRANS_FILE) + if out[0] in '-,;[]{}()~`@#': + out = '_%s' % out + return out + + +def FileOpen(parent, message, default_dir=None, + default_file=None, multiple=False, + wildcard=None): + """File Open dialog wrapper. + returns full path on OK or None on Cancel + """ + out = None + if default_dir is None: + default_dir = os.getcwd() + if wildcard is None: + wildcard = 'All files (*.*)|*.*' + + style = wx.FD_OPEN|wx.FD_CHANGE_DIR + if multiple: + style = style|wx.MULTIPLE + dlg = wx.FileDialog(parent, message=message, + defaultFile=default_file, + defaultDir=default_dir, + wildcard=wildcard, + style=style) + + out = None + if dlg.ShowModal() == wx.ID_OK: + out = os.path.abspath(dlg.GetPath()) + dlg.Destroy() + return out + +def FileSave(parent, message, default_file=None, + default_dir=None, wildcard=None): + "File Save dialog" + out = None + if wildcard is None: + wildcard = 'All files (*.*)|*.*' + + if default_dir is None: + default_dir = os.getcwd() + + dlg = wx.FileDialog(parent, message=message, + defaultFile=default_file, + wildcard=wildcard, + style=wx.FD_SAVE|wx.FD_CHANGE_DIR) + if dlg.ShowModal() == wx.ID_OK: + out = os.path.abspath(dlg.GetPath()) + dlg.Destroy() + return out + + +def SelectWorkdir(parent, message='Select Working Folder...'): + "prompt for and change into a working directory " + dlg = wx.DirDialog(parent, message, + style=wx.DD_DEFAULT_STYLE|wx.DD_CHANGE_DIR) + + path = os.path.abspath(os.curdir) + dlg.SetPath(path) + if dlg.ShowModal() == wx.ID_CANCEL: + return None + path = os.path.abspath(dlg.GetPath()) + dlg.Destroy() + os.chdir(path) + return path + +class Closure: + """A very simple callback class to emulate a closure (reference to + a function with arguments) in python. + + This class holds a user-defined function to be executed when the + class is invoked as a function. This is useful in many situations, + especially for 'callbacks' where lambda's are quite enough. + Many Tkinter 'actions' can use such callbacks. + + >>>def my_action(x=None): + ... print('my action: x = ', x) + >>>c = Closure(my_action,x=1) + ..... sometime later ... + >>>c() + my action: x = 1 + >>>c(x=2) + my action: x = 2 + + based on Command class from J. Grayson's Tkinter book. + """ + def __init__(self, func=None, *args, **kws): + self.func = func + self.kws = kws + self.args = args + + def __call__(self, *args, **kws): + self.kws.update(kws) + if callable(self.func): + self.args = args + return self.func(*self.args, **self.kws) + + +class FloatCtrl(wx.TextCtrl): + """ Numerical Float Control:: + a wx.TextCtrl that allows only numerical input, can take a precision argument + and optional upper / lower bounds + Options: + + """ + def __init__(self, parent, value='', minval=None, maxval=None, + precision=3, bell_on_invalid = True, + act_on_losefocus=False, + action=None, action_kw=None, **kws): + + self.__digits = '0123456789.-' + self.__prec = precision + if precision is None: + self.__prec = 0 + self.format = '%%.%if' % self.__prec + self.is_valid = True + self.__val = set_float(value) + self.__max = set_float(maxval) + self.__min = set_float(minval) + self.__bound_val = None + self.__mark = None + self.__action = None + + self.fgcol_valid = "Black" + self.bgcol_valid = "White" + self.fgcol_invalid = "Red" + self.bgcol_invalid = (254, 254, 80) + self.bell_on_invalid = bell_on_invalid + self.act_on_losefocus = act_on_losefocus + + # set up action + if action_kw is None: + action_kw = {} + self.SetAction(action, **action_kw) + + this_sty = wx.TE_PROCESS_ENTER|wx.TE_RIGHT + if 'style' in kws: + this_sty = this_sty | kws['style'] + kws['style'] = this_sty + + wx.TextCtrl.__init__(self, parent, wx.ID_ANY, **kws) + + self.__CheckValid(self.__val) + self.SetValue(self.__val) + + self.Bind(wx.EVT_CHAR, self.OnChar) + self.Bind(wx.EVT_TEXT, self.OnText) + + self.Bind(wx.EVT_SET_FOCUS, self.OnSetFocus) + self.Bind(wx.EVT_KILL_FOCUS, self.OnKillFocus) + self.__GetMark() + + def SetAction(self, action, **kws): + "set callback action" + if callable(action): + self.__action = Closure(action, **kws) + + def SetPrecision(self, prec=0): + "set precision" + self.__prec = prec + self.format = '%%.%if' % prec + + def __GetMark(self): + " keep track of cursor position within text" + try: + self.__mark = min(wx.TextCtrl.GetSelection(self)[0], + len(wx.TextCtrl.GetValue(self).strip())) + except: + self.__mark = 0 + + def __SetMark(self, mark=None): + "set mark for later" + if mark is None: + mark = self.__mark + self.SetSelection(mark, mark) + + def SetValue(self, value=None, act=True): + " main method to set value " + if value is None: + value = wx.TextCtrl.GetValue(self).strip() + self.__CheckValid(value) + self.__GetMark() + value = set_float(value) + if value is not None: + wx.TextCtrl.SetValue(self, self.format % value) + + 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() + + self.__SetMark() + + def OnKillFocus(self, event): + "focus lost" + self.__GetMark() + if self.act_on_losefocus and callable(self.__action): + self.__action(value=self.__val) + event.Skip() + + def OnSetFocus(self, event): + "focus gained - resume editing from last mark point" + self.__SetMark() + event.Skip() + + def OnChar(self, event): + """ on Character event""" + key = event.GetKeyCode() + entry = wx.TextCtrl.GetValue(self).strip() + pos = wx.TextCtrl.GetSelection(self) + # really, the order here is important: + # 1. return sends to ValidateEntry + if key == wx.WXK_RETURN: + if not self.is_valid: + wx.TextCtrl.SetValue(self, self.format % set_float(self.__bound_val)) + else: + self.SetValue(entry) + return + + # 2. other non-text characters are passed without change + if (key < wx.WXK_SPACE or key == wx.WXK_DELETE or key > 255): + event.Skip() + return + + # 3. check for multiple '.' and out of place '-' signs and ignore these + # note that chr(key) will now work due to return at #2 + + has_minus = '-' in entry + ckey = chr(key) + if ((ckey == '.' and (self.__prec == 0 or '.' in entry) ) or + (ckey == '-' and (has_minus or pos[0] != 0)) or + (ckey != '-' and has_minus and pos[0] == 0)): + return + # 4. allow digits, but not other characters + if chr(key) in self.__digits: + event.Skip() + + + def OnText(self, event=None): + "text event" + try: + if event.GetString() != '': + self.__CheckValid(event.GetString()) + except: + pass + event.Skip() + + def GetValue(self): + if self.__prec > 0: + return set_float("%%.%ig" % (self.__prec) % (self.__val)) + else: + return int(self.__val) + + def GetMin(self): + "return min value" + return self.__min + + def GetMax(self): + "return max value" + return self.__max + + def SetMin(self, val): + "set min value" + self.__min = set_float(val) + + def SetMax(self, val): + "set max value" + self.__max = set_float(val) + + def __CheckValid(self, value): + "check for validity of value" + val = self.__val + self.is_valid = True + try: + val = set_float(value) + if self.__min is not None and (val < self.__min): + self.is_valid = False + val = self.__min + if self.__max is not None and (val > self.__max): + self.is_valid = False + val = self.__max + except: + self.is_valid = False + self.__bound_val = self.__val = val + fgcol, bgcol = self.fgcol_valid, self.bgcol_valid + if not self.is_valid: + fgcol, bgcol = self.fgcol_invalid, self.bgcol_invalid + + self.SetForegroundColour(fgcol) + self.SetBackgroundColour(bgcol) + self.Refresh() + + +class NumericCombo(wx.ComboBox): + """ + Numeric Combo: ComboBox with numeric-only choices + """ + def __init__(self, parent, choices, precision=3, + init=0, width=80): + + self.fmt = "%%.%if" % precision + self.choices = choices + schoices = [self.fmt % i for i in self.choices] + wx.ComboBox.__init__(self, parent, -1, '', (-1, -1), (width, -1), + schoices, wx.CB_DROPDOWN|wx.TE_PROCESS_ENTER) + + init = min(init, len(self.choices)) + self.SetStringSelection(schoices[init]) + self.Bind(wx.EVT_TEXT_ENTER, self.OnEnter) + + def OnEnter(self, event=None): + "text enter event handler" + thisval = float(event.GetString()) + + if thisval not in self.choices: + self.choices.append(thisval) + self.choices.sort() + + self.Clear() + self.AppendItems([self.fmt % i for i in self.choices]) + self.SetSelection(self.choices.index(thisval)) + +class SimpleText(wx.StaticText): + "simple static text wrapper" + def __init__(self, parent, label, minsize=None, + font=None, colour=None, bgcolour=None, + style=wx.ALIGN_CENTRE, **kws): + + wx.StaticText.__init__(self, parent, -1, + label=label, style=style, **kws) + + if minsize is not None: + self.SetMinSize(minsize) + if font is not None: + self.SetFont(font) + if colour is not None: + self.SetForegroundColour(colour) + if bgcolour is not None: + self.SetBackgroundColour(colour) + +class HyperText(wx.StaticText): + """HyperText is a simple extension of wx.StaticText that + + 1. adds an underscore to the lable to appear to be a hyperlink + 2. performs the supplied action on Left-Up button events + """ + def __init__(self, parent, label, action=None, colour=(50, 50, 180)): + self.action = action + wx.StaticText.__init__(self, parent, -1, label=label) + font = self.GetFont() # .Bold() + font.SetUnderlined(True) + self.SetFont(font) + self.SetForegroundColour(colour) + self.Bind(wx.EVT_LEFT_UP, self.OnSelect) + + def OnSelect(self, evt=None): + "Left-Up Event Handler" + if self.action is not None: + self.action(evt=evt, label=self.GetLabel()) + evt.Skip() + +class DateTimeCtrl(object): + """ + Simple Combined date/time control + """ + def __init__(self, parent, name='datetimectrl', use_now=False): + self.name = name + panel = self.panel = wx.Panel(parent) + bgcol = wx.Colour(250, 250, 250) + + datestyle = wx.DP_DROPDOWN|wx.DP_SHOWCENTURY|wx.DP_ALLOWNONE + + self.datectrl = wx.DatePickerCtrl(panel, size=(120, -1), + style=datestyle) + self.timectrl = masked.TimeCtrl(panel, -1, name=name, + limited=False, + fmt24hr=True, oob_color=bgcol) + timerheight = self.timectrl.GetSize().height + spinner = wx.SpinButton(panel, -1, wx.DefaultPosition, + (-1, timerheight), wx.SP_VERTICAL ) + self.timectrl.BindSpinButton(spinner) + sizer = wx.BoxSizer(wx.HORIZONTAL) + sizer.Add(self.datectrl, 0, wx.ALIGN_CENTER) + sizer.Add(self.timectrl, 0, wx.ALIGN_CENTER) + sizer.Add(spinner, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALIGN_LEFT) + panel.SetSizer(sizer) + sizer.Fit(panel) + if use_now: + self.timectrl.SetValue(wx.DateTime_Now()) diff --git a/epics/wx/wxlib.py b/epics/wx/wxlib.py new file mode 100644 index 0000000..1d87baa --- /dev/null +++ b/epics/wx/wxlib.py @@ -0,0 +1,1226 @@ +""" +wx utility functions for Epics and wxPython interaction +""" +import wx +try: + from wx._core import PyDeadObjectError +except: + PyDeadObjectError = Exception + +import six +import time +import sys +import epics +import wx.lib.buttons as buttons +import wx.lib.agw.floatspin as floatspin + +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 + Epics calls can be made in a separate thread, and asynchronously. + + This decorator should be used for all code that mix calls to + wx and epics + """ + def wrapper(*args, **kwargs): + "callafter wrapper" + try: + wx.CallAfter(f, *args, **kwargs) + except PyDeadObjectError: + pass + return wrapper + +def DelayedEpicsCallback(fcn): + """decorator to wrap an Epics callback in a wx.CallAfter, + so that the wx and epics ca threads do not clash + This also checks for dead wxPython objects (say, from a + closed window), and remove callbacks to them. + """ + def wrapper(*args, **kw): + "callafter wrapper" + def cb(): + "default callback" + try: + fcn(*args, **kw) + except PyDeadObjectError: + cb_index, pv = kw.get('cb_info', (None, None)) + if hasattr(pv, 'remove_callback'): + try: + pv.remove_callback(index=cb_index) + except RuntimeError: + pass + if wx.GetApp() is not None: + return wx.CallAfter(cb) + return wrapper + +@EpicsFunction +def finalize_epics(): + """explicitly finalize and cleanup epics so as to + prevent core-dumps on exit. + """ + epics.ca.finalize_libca() + epics.ca.poll() + +class EpicsTimer: + """ Epics Event Timer: + combines a wxTimer and epics.ca.pend_event to cause Epics Event Processing + within a wx Application. + + >>> my_timer = EpicsTimer(parent, period=100) + + period is in milliseconds. At each period, epics.ca.poll() will be run. + + """ + def __init__(self, parent, period=100, start=True): + self.parent = parent + self.period = period + self.timer = wx.Timer(parent) + self.parent.Bind(wx.EVT_TIMER, self.pend) + if start: + self.StartTimer() + + def StopTimer(self): + "stop timer" + self.timer.Stop() + + def StartTimer(self): + "start timer" + self.timer.Start(self.period) + + @EpicsFunction + def pend(self, event=None): + "pend/poll" + epics.ca.poll() + event.Skip() + +class PVMixin(object): + """ base class mixin for any class that needs PV wx callback + support. + + If you're working with wxwidgets controls, see PVCtrlMixin. + If you're working with wx OGL drawing, see ogllib.pvShapeMixin. + + Classes deriving directly from PVMixin must override OnPVChange() + """ + def __init__(self, pv=None, pvname=None): + self.pv = None + if pv is None and pvname is not None: + pv = pvname + if pv is not None: + self.SetPV(pv) + + self._popupmenu = None + self.Bind(wx.EVT_RIGHT_DOWN, self._onRightDown) + + def _onRightDown(self, event=None): + """right button down: show pop-up""" + if event is None: + return + if self._popupmenu is None: + self.build_popupmenu() + wx.CallAfter(self.PopupMenu, self._popupmenu, event.GetPosition()) + event.Skip() + + def build_popupmenu(self, event=None): + self._copy_name = wx.NewId() + self._copy_val = wx.NewId() + self._show_info = wx.NewId() + self._popupmenu = wx.Menu() + self._popupmenu.Append(self._copy_name, 'Copy PV Name to Clipboard') + self._popupmenu.Append(self._copy_val, 'Copy PV Value to Clipboard') + self._popupmenu.AppendSeparator() + self._popupmenu.Append(self._show_info, 'Show PV Info') + + self.Bind(wx.EVT_MENU, self.copy_name, id=self._copy_name) + self.Bind(wx.EVT_MENU, self.copy_val, id=self._copy_val) + self.Bind(wx.EVT_MENU, self.show_info, id=self._show_info) + + @EpicsFunction + def copy_name(self, event=None): + dat = wx.TextDataObject() + dat.SetText(self.pv.pvname) + wx.TheClipboard.Open() + wx.TheClipboard.SetData(dat) + wx.TheClipboard.Close() + + @EpicsFunction + def copy_val(self, event=None): + dat = wx.TextDataObject() + dat.SetText(self.pv.get(as_string=True)) + wx.TheClipboard.Open() + wx.TheClipboard.SetData(dat) + wx.TheClipboard.Close() + + @EpicsFunction + def show_info(self, event=None): + dlg = wx.MessageDialog(self, self.pv.info, + 'Info for %s' % self.pv.pvname, + wx.OK|wx.ICON_INFORMATION) + dlg.ShowModal() + dlg.Destroy() + + @EpicsFunction + def SetPV(self, pv=None): + "set pv, either an epics.PV object or a pvname" + if pv is None: + return + if isinstance(pv, epics.PV): + self.pv = pv + 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() + + epics.poll() + self.pv.connection_callbacks.append(self.OnEpicsConnect) + if self.pv.connected: + self.OnEpicsConnect(pvname=self.pv.pvname, conn=True, pv=self.pv) + + self.pv.get_ctrlvars() + + self.OnPVChange(self.pv.get(as_string=True)) + ncback = len(self.pv.callbacks) + 1 + self.pv.add_callback(self._pvEvent, wid=self.GetId(), cb_info=ncback) + # print " PVMixin add callback for ", self.pv, self.pv.chid, len(self.pv.callbacks) + + @DelayedEpicsCallback + def OnEpicsConnect(self, pvname=None, conn=None, pv=None): + """Connect Callback: + Enable/Disable widget on change in connection status + """ + pass + + @DelayedEpicsCallback + def _pvEvent(self, pvname=None, value=None, wid=None, + char_value=None, **kws): + "epics PV callback function" + if pvname is None or value is None or wid is None: + return + if char_value is None and value is not None: + prec = kws.get('precision', None) + if prec not in (None, 0): + char_value = ("%%.%if" % prec) % value + else: + char_value = set_float(value) + self.OnPVChange(char_value) + + @EpicsFunction + def Update(self, value=None): + "update value" + if value is None and self.pv is not None: + value = self.pv.get(as_string=True) + self.OnPVChange(value) + + @EpicsFunction + def GetValue(self, as_string=True): + "return value" + val = self.pv.get(as_string=as_string) + result = self.translations.get(val, val) + return result + + def OnPVChange(self, str_value): + """method is called any time the PV value changes, via + Update() or via a PV callback + """ + self._warn("Must override OnPVChange") + + def _warn(self, msg): + "write warning" + sys.stderr.write("%s for pv='%s'\n" % (msg, self.pv.pvname)) + + @EpicsFunction + def GetEnumStrings(self): + """try to get list of enum strings, + returns enum strings or None""" + epics.poll() + out = None + if isinstance(self.pv, epics.PV): + self.pv.get_ctrlvars() + if self.pv.type == 'enum': + out =list(self.pv.enum_strs) + return out + +class PVCtrlMixin(PVMixin): + """ + mixin for wx Controls with epics PVs: connects to PV, + and manages callback events for the PV + + An overriding class must provide a method called _SetValue, which + will set the contents of the corresponding widget. + + + Optional Features for descendents + ================================= + + * Set a translation dictionary of PVValue->Python Value to be used + whenever values are received via PV callbacks. + + * Set translation tables for setting particular foreground/background + colours when the PV takes certain values. + + * Override foreground/background colours - without knowing what + colour is currently set by the user, you can call + OverrideForegroundColour()/OverrideBackGroundColor() to set a + different colour on the control and then call the override again + with None to go back to the original colour. + + """ + + def __init__(self, pv=None, pvname=None, font=None, fg=None, bg=None): + PVMixin.__init__(self, pv, pvname) + + self._translations = {} + self._fg_colour_translations = None + self._bg_colour_translations = None + self._fg_colour_alarms = {} + self._bg_colour_alarms = {} + self._default_fg_colour = None + self._default_bg_colour = None + + try: + if font is not None: + self.SetFont(font) + if fg is not None: + self.SetForegroundColour(fg) + if bg is not None: + self.SetBackgroundColour(fg) + except: + pass + self._connect_bgcol = self.GetBackgroundColour() + self._connect_fgcol = self.GetForegroundColour() + + @DelayedEpicsCallback + def OnEpicsConnect(self, pvname=None, conn=None, pv=None): + """Connect Callback: + Enable/Disable widget on change in connection status + """ + PVMixin.OnEpicsConnect(self, pvname, conn, pv) + action = getattr(self, 'Enable', None) + bgcol = self._connect_bgcol + fgcol = self._connect_fgcol + if not conn: + action = getattr(self, 'Disable', None) + self._connect_bgcol = self.GetBackgroundColour() + self._connect_fgcol = self.GetForegroundColour() + bgcol = wx.Colour(240, 240, 210) + fgcol = wx.Colour(200, 100, 100) + if action is not None: + self.SetBackgroundColour(bgcol) + self.SetForegroundColour(fgcol) + action() + + + + def SetTranslations(self, translations): + """ + Pass a dictionary of value->value translations here if you want some P + PV values to automatically appear in the event callback as a different + value. + + ie, to override PV value 0.0 to say "Disabled", call this method as + control.SetTranslations({ 0.0 : "Disabled" }) + + It is recommended that you use this function only when it is not + possible to change the PV value in the database, or set a string + value in the database. + """ + self._translations = translations + + def SetForegroundColourTranslations(self, translations): + """ + Pass a dictionary of value->colour translations here if you want the + control to automatically set foreground colour based on PV value. + + Values used to lookup colours will be string values if available, + but will otherwise be the raw PV value. + + Colour values in the dictionary may be strings or wx.Colour objects. + """ + self._fg_colour_translations = translations + + def SetBackgroundColourTranslations(self, translations): + """ + Pass a dictionary of value->colour translations here if you want the + control to automatically set background colour based on PV value. + + Values used to lookup colours will be string values if available, + but will otherwise be the raw PV value. + + Colour values in the dictionary may be strings or wx.Colour objects. + + """ + self._bg_colour_translations = translations + + def SetForegroundColour(self, colour): + """ (Internal override) Needed to support OverrideForegroundColour() + """ + if self._default_fg_colour is None: + wx.Window.SetForegroundColour(self, colour) + else: + self._default_fg_colour = colour + + def GetForegroundColour(self): + """ (Internal override) Needed to support OverrideForegroundColour() + """ + return self._default_fg_colour if self._default_fg_colour is not None \ + else wx.Window.GetForegroundColour(self) + + def SetBackgroundColour(self, colour): + """ (Internal override) Needed to support OverrideBackgroundColour() + """ + if self._default_bg_colour is None: + wx.Window.SetBackgroundColour(self, colour) + else: + self._default_bg_colour = colour + + def GetBackgroundColour(self): + """ (Internal override) Needed to support OverrideBackgroundColour() + """ + return self._default_bg_colour if self._default_bg_colour is not None \ + else wx.Window.GetBackgroundColour(self) + + def OverrideForegroundColour(self, colour): + """ + Call this method to override the control's current set foreground + colour. Call with colour=None to disable overriding and go back to + whatever was set. + + Overriding allows SetForegroundColour() to still work as expected, + except when the "override" is set. + """ + if colour is None: + if self._default_fg_colour is not None: + wx.Window.SetForegroundColour(self, self._default_fg_colour) + self._default_fg_colour = None + else: + if self._default_fg_colour is None: + self._default_fg_colour = wx.Window.GetForegroundColour(self) + wx.Window.SetForegroundColour(self, colour) + + def OverrideBackgroundColour(self, colour): + """ + Call this method to override the control's current set background + colour, Call with colour=None to disable overriding and go back to + whatever was set. + + Overriding allows SetForegroundColour() to still work as expected, + except when the "override" is set. + """ + if colour is None: + if self._default_bg_colour is not None: + wx.Window.SetBackgroundColour(self, self._default_bg_colour) + else: + if self._default_bg_colour is None: + self._default_bg_colour = wx.Window.GetBackgroundColour(self) + wx.Window.SetBackgroundColour(self, colour) + + def _SetValue(self, value): + "set value -- must override" + self._warn("must override _SetValue") + + @EpicsFunction + def OnPVChange(self, raw_value): + "called by PV callback" + try: + if self.pv is None: + return + if self.pv.form == "native" \ + and ( len(self._fg_colour_alarms) > 0 or + len(self._bg_colour_alarms) > 0 ): + # native PVs don't update severity on callback, + # so we need to do the same manually + self.pv.get_ctrlvars() + + colour = None + if self._fg_colour_translations is not None and \ + raw_value in self._fg_colour_translations: + colour = self._fg_colour_translations[raw_value] + elif self.pv.severity in self._fg_colour_alarms: + colour = self._fg_colour_alarms[self.pv.severity] + self.OverrideForegroundColour(colour) + + colour = None + if self._bg_colour_translations is not None and \ + raw_value in self._bg_colour_translations: + colour = self._bg_colour_translations[raw_value] + elif self.pv.severity in self._bg_colour_alarms: + colour = self._bg_colour_alarms[self.pv.severity] + self.OverrideBackgroundColour(colour) + try: + self._SetValue(self._translations.get(raw_value, raw_value)) + except TypeError: + pass + except PyDeadObjectError: + pass + + +class PVTextCtrl(wx.TextCtrl, PVCtrlMixin): + """ + Text control (ie textbox) for PV display (as normal string), + with callback for automatic updates and option to write value + back on input + """ + def __init__(self, parent, pv=None, + font=None, fg=None, bg=None, dirty_timeout=2500, **kws): + + if 'style' not in kws: + kws['style'] = wx.TE_PROCESS_ENTER + else: + kws['style'] |= wx.TE_PROCESS_ENTER + + wx.TextCtrl.__init__(self, parent, wx.ID_ANY, value='', **kws) + PVCtrlMixin.__init__(self, pv=pv, font=font, fg=fg, bg=bg) + self.Bind(wx.EVT_CHAR, self.OnChar) + self.dirty_timeout = dirty_timeout + self.dirty_writeback_timer = wx.Timer(self) + self.Bind(wx.EVT_TIMER, self.OnWriteback) + self.Bind(wx.EVT_KILL_FOCUS, self.OnWriteback) + self.Bind(wx.EVT_CHAR, self.OnChar) + + + @EpicsFunction + def _caput(self, value): + "epics pv.put wrapper" + self.pv.put(str(value)) + + def SetValue(self, value): + "override all setvalue" + self._caput(value) + + def _SetValue(self, value): + "set widget value" + wx.TextCtrl.SetValue(self, value) + + def OnChar(self, event): + "char event handler" + if event.KeyCode == wx.WXK_RETURN: + self.OnWriteback() + else: + if self.dirty_timeout is not None: + self.dirty_writeback_timer.Start(self.dirty_timeout) + event.Skip() + + def OnWriteback(self, event=None): + """ writeback the currently displayed value to the PV """ + self.dirty_writeback_timer.Stop() + entry = str(self.GetValue().strip()) + self.SetValue(entry) + +class PVText(wx.StaticText, PVCtrlMixin): + """ Static text for displaying a PV value, + with callback for automatic updates + + By default the text colour will change on alarm states. + This can be overriden or disabled as constructor + parameters + """ + def __init__(self, parent, pv=None, as_string=True, + font=None, fg=None, bg=None, style=None, + minor_alarm="DARKRED", major_alarm="RED", + invalid_alarm="ORANGERED", auto_units=False, units="", **kw): + """ + Create a new pvText + + minor_alarm, major_alarm & invalid_alarm are all text colours + that will be set depending no the alarm state of the target + PV. Set to None if you want no highlighting in that alarm state. + + auto_units means the PV value will be displayed with the EGU + "engineering units" as a suffix. Alternately, you can specify + an explicit unit string. + """ + + wstyle = wx.ALIGN_LEFT + if style is not None: + wstyle = style + + wx.StaticText.__init__(self, parent, wx.ID_ANY, label='', + style=wstyle, **kw) + PVCtrlMixin.__init__(self, pv=pv, font=font, fg=fg, bg=bg) + + self.as_string = as_string + self.auto_units = auto_units + self.units = units + + 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" + if self.auto_units and self.pv.units: + self.units = " " + self.pv.units + if value is not None: + self.SetLabel("%s%s" % (value, self.units)) + + +class PVStaticText(wx.StaticText, PVMixin): + """ Static text for displaying a PV value, + with callback for automatic updates + + By default the text colour will change on alarm states. + This can be overriden or disabled as constructor + parameters + """ + 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 + + 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" + if value is not None: + self.SetLabel("%s" % value) + + @EpicsFunction + def OnPVChange(self, value): + "called by PV callback" + try: + if self.pv is None: + return + self.SetLabel(value) + except: + pass + +class PVEnumButtons(wx.Panel, PVCtrlMixin): + """ a panel of buttons for Epics ENUM controls """ + def __init__(self, parent, pv=None, + orientation=wx.HORIZONTAL, **kw): + + self.orientation = orientation + wx.Panel.__init__(self, parent, wx.ID_ANY, **kw) + PVCtrlMixin.__init__(self, pv=pv) + + @EpicsFunction + def SetPV(self, pv=None): + "set pv, either an epics.PV object or a pvname" + if pv is None: + return + if isinstance(pv, epics.PV): + self.pv = pv + elif isinstance(pv, six.string_types): + self.pv = epics.get_pv(pv) + self.pv.connect() + + epics.poll() + self.pv.connection_callbacks.append(self.OnEpicsConnect) + + self.pv.get_ctrlvars() + + self.OnPVChange(self.pv.get(as_string=True)) + ncback = len(self.pv.callbacks) + 1 + self.pv.add_callback(self._pvEvent, wid=self.GetId(), cb_info=ncback) + + pv_value = pv.get(as_string=True) + enum_strings = pv.enum_strs + + sizer = wx.BoxSizer(self.orientation) + self.buttons = [] + if enum_strings is not None: + for i, label in enumerate(enum_strings): + b = buttons.GenToggleButton(self, -1, label) + self.buttons.append(b) + b.Bind(wx.EVT_BUTTON, Closure(self._onButton, index=i) ) + b.Bind(wx.EVT_RIGHT_DOWN, self._onRightDown) + sizer.Add(b, flag = wx.ALL) + b.SetToggle(label==pv_value) + + self.SetAutoLayout(True) + self.SetSizer(sizer) + sizer.Fit(self) + + + @EpicsFunction + def _onButton(self, event=None, index=None, **kw): + "button event handler" + if self.pv is not None and index is not None: + self.pv.put(index) + + @DelayedEpicsCallback + def _pvEvent(self, pvname=None, value=None, wid=None, **kw): + "pv event handler" + if pvname is None or value is None: + return + for i, btn in enumerate(self.buttons): + btn.up = (i != value) + btn.Refresh() + + def _SetValue(self, value): + "not implemented" + pass + +class PVEnumChoice(wx.Choice, PVCtrlMixin): + """ a dropdown choice for Epics ENUM controls """ + + def __init__(self, parent, pv=None, **kw): + wx.Choice.__init__(self, parent, wx.ID_ANY, **kw) + PVCtrlMixin.__init__(self, pv=pv) + + @EpicsFunction + def SetPV(self, pv=None): + "set pv, either an epics.PV object or a pvname" + if pv is None: + return + if isinstance(pv, epics.PV): + self.pv = pv + elif isinstance(pv, six.string_types): + self.pv = epics.get_pv(pv) + self.pv.connect() + + epics.poll() + self.pv.connection_callbacks.append(self.OnEpicsConnect) + + self.pv.get_ctrlvars() + + self.OnPVChange(self.pv.get(as_string=True)) + ncback = len(self.pv.callbacks) + 1 + self.pv.add_callback(self._pvEvent, wid=self.GetId(), cb_info=ncback) + + self.Bind(wx.EVT_CHOICE, self._onChoice) + + pv_value = pv.get(as_string=True) + enum_strings = pv.enum_strs + + self.Clear() + self.AppendItems(enum_strings) + self.SetStringSelection(pv_value) + + def _onChoice(self, event=None, **kws): + "choice event handler" + if self.pv is not None: + index = self.pv.enum_strs.index(event.GetString()) + self.pv.put(index) + + @DelayedEpicsCallback + def _pvEvent(self, pvname=None, value=None, **kw): + "pv event handler" + if value is not None: + self.SetSelection(value) + + def _SetValue(self, value): + "set value" + self.SetStringSelection(value) + +class PVAlarm(wx.MessageDialog, PVCtrlMixin): + """ Alarm Message for a PV: a MessageDialog will pop up when a + PV trips some alarm level""" + + def __init__(self, parent, pv=None, + font=None, fg=None, bg=None, trip_point=None, **kw): + + PVCtrlMixin.__init__(self, pv=pv, font=font, fg=None, bg=None) + + def _SetValue(self, value): + "set value -- null op for pvAlarm" + pass + +class PVFloatCtrl(FloatCtrl, PVCtrlMixin): + """ Float control for PV display of numerical data, + with callback for automatic updates, and + automatic determination of string/float controls + + Options: + parent wx widget of parent + pv epics pv to use for value + precision number of digits past decimal point to display + (default to PV's precision) + font wx font + fg wx foreground colour + bg wx background colour + + bell_on_invalid ring bell when input is out of range + """ + def __init__(self, parent, pv=None, + font=None, fg=None, bg=None, precision=None, **kw): + + self.pv = None + FloatCtrl.__init__(self, parent, value=0, + precision=precision, **kw) + PVCtrlMixin.__init__(self, pv=pv, font=font, fg=None, bg=None) + + def _SetValue(self, value): + "set widget value" + FloatCtrl.SetValue(self, value, act=False) + + @EpicsFunction + 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, six.string_types): + self.pv = epics.get_pv(pv) + if self.pv is None: + return + self.pv.connection_callbacks.append(self.OnEpicsConnect) + self.pv.get() + self.pv.get_ctrlvars() + # be sure to set precision before value!! or PV may be moved!! + prec = self.pv.precision + if prec is None: + prec = 0 + self.SetPrecision(prec) + + self.SetValue(self.pv.char_value, act=False) + + if self.pv.type in ('string', 'char'): + try: + x = float(self.pv.value) + except: + self._warn('pvFloatCtrl needs a double or float PV') + + llim = set_float(self.pv.lower_ctrl_limit) + hlim = set_float(self.pv.upper_ctrl_limit) + if hlim is not None and llim is not None and hlim > llim: + self.SetMax(hlim) + self.SetMin(llim) + ncback = len(self.pv.callbacks) + 1 + self.pv.add_callback(self._pvEvent, wid=self.GetId(), cb_info=ncback) + self.SetAction(self._onEnter) + + @DelayedEpicsCallback + def _FloatpvEvent(self, pvname=None, value=None, wid=None, + char_value=None, **kw): + "PV callback / event handler for pv change" + + if pvname is None or value is None or wid is None: + return + if char_value is None and value is not None: + prec = kw.get('precision', None) + if prec not in (None, 0): + char_value = ("%%.%if" % prec) % value + else: + char_value = set_float(value) + self.SetValue(char_value, act=False) + + @EpicsFunction + def _onEnter(self, value=None, **kw): + "enter/return event" + if value in (None,'') or self.pv is None: + return + try: + if float(value) != self.pv.get(): + self.pv.put(float(value)) + except: + pass + +class PVBitmap(wx.StaticBitmap, PVCtrlMixin): + """ + Static Bitmap where image is based on PV value, + with callback for automatic updates + + """ + def __init__(self, parent, pv=None, bitmaps=None, + defaultBitmap=None, **kw): + """ + bitmaps - a dict of Value->Bitmap mappings, to automatically change + the shown bitmap based on the PV value. + + defaultBitmap - the bitmap to show if the PV value doesn't match any + of the values in the bitmaps dict. + + """ + wx.StaticBitmap.__init__(self, parent, wx.ID_ANY, + bitmap=defaultBitmap, **kw) + PVCtrlMixin.__init__(self, pv=pv) + + self.defaultBitmap = defaultBitmap + if bitmaps is None: + bitmaps = {} + self.bitmaps = bitmaps + + + def _SetValue(self, value): + "set widget value" + if value in self.bitmaps: + nextBitmap = self.bitmaps[value] + else: + nextBitmap = self.defaultBitmap + if nextBitmap != self.GetBitmap(): + self.SetBitmap(nextBitmap) + +class PVCheckBox(wx.CheckBox, PVCtrlMixin): + """ + Checkbox based on a binary PV value, both reads/writes the + PV on changes. + + There are multiple options for translating PV values to checkbox + settings (from least to most complex): + + * Use a PV with values 0 and 1 + * Use a PV with values that convert via Python's own bool(x) + * Set on_value and off_value in the constructor + * Use SetTranslations() to set a dictionary for converting various + PV values to booleans. + + """ + def __init__(self, parent, pv=None, on_value=1, off_value=0, **kw): + self.pv = None + wx.CheckBox.__init__(self, parent, **kw) + PVCtrlMixin.__init__(self, pv=pv, font="", fg=None, bg=None) + wx.EVT_CHECKBOX(parent, self.GetId(), self.OnClicked) + self.on_value = on_value + self.off_value = off_value + self.OnChange = None + + def _SetValue(self, value): + "set widget value" + if value in (self.on_value, self.off_value): + self.Value = (value == self.on_value) + else: + self.Value = bool(self.pv.get()) + if callable(self.OnChange): + self.OnChange(self) + + @EpicsFunction + def OnClicked(self, event=None): + "checkbox event handler" + if self.pv is not None: + self.pv.put(self.on_value if self.Value else self.off_value) + + def SetValue(self, new_value): + "set widget value" + old_value = self.Value + wx.CheckBox.SetValue(self, new_value) + if old_value != new_value: + self.OnClicked(None) + + # need to redefine the value Property as the old property refs old SetValue + Value = property(wx.CheckBox.GetValue, SetValue) + +class PVFloatSpin(floatspin.FloatSpin, PVCtrlMixin): + """ + A FloatSpin (floating-point-aware SpinCtrl) linked to a PV, + both reads and writes the PV on changes. + + """ + def __init__(self, parent, pv=None, deadTime=2500, + min_val=None, max_val=None, increment=1.0, digits=-1, **kw): + """ + Most arguments are common with FloatSpin. + + Additional Arguments: + pv = pv to set + deadTime = delay (ms) between user typing a value into the field, + and it being set to the PV + + """ + floatspin.FloatSpin.__init__(self, parent, increment=increment, + min_val=min_val, max_val=max_val, + digits=digits, **kw) + PVCtrlMixin.__init__(self, pv=pv, font="", fg=None, bg=None) + floatspin.EVT_FLOATSPIN(parent, self.GetId(), self.OnSpin) + + self.deadTimer = wx.Timer(self) + self.deadTime = deadTime + wx.EVT_TIMER(self, self.deadTimer.GetId(), self.OnTimeout) + + @EpicsFunction + def _SetValue(self, value): + "set value" + floatspin.FloatSpin.SetValue(self, float(self.pv.get())) + + @EpicsFunction + def OnSpin(self, event=None): + "spin event handler" + if self.pv is not None: + value = self.GetValue() + if self.pv.upper_ctrl_limit != 0 or self.pv.lower_ctrl_limit != 0: + # both zero -> not set + if value > self.pv.upper_ctrl_limit: + value = self.pv.upper_ctrl_limit + if value < self.pv.lower_ctrl_limit: + value = self.pv.lower_ctrl_limit + self.SetValue(value) + + def OnTimeout(self, event): + "timer event handler" + # save & restore insertion point before syncing control + savePoint = self.GetTextCtrl().InsertionPoint + self.SyncSpinToText() + self.GetTextCtrl().InsertionPoint = savePoint + self.OnSpin(event) + + def OnChar(self, event): + "floatspin char event" + floatspin.FloatSpin.OnChar(self, event) + # Timer will restart if it's already running + self.deadTimer.Start(milliseconds=self.deadTime, oneShot=True) + + def SetValue(self, value): + floatspin.FloatSpin.SetValue(self, value) + if hasattr(self, "pv"): # can be called before PV is assigned + self.pv.put(value) + + +class PVSpinCtrl(wx.SpinCtrl, PVCtrlMixin): + """ + A SpinCtrl linked to a PV, + both reads and writes the PV on changes. + + """ + def __init__(self, parent, pv=None, + min_val=None, max_val=None, **kws): + """ + Most arguments are common with SpinCtrl + + Additional Arguments: + pv = pv to set + deadTime = delay (ms) between user typing a value into the field, + and it being set to the PV + + """ + wx.SpinCtrl.__init__(self, parent, **kws) + PVCtrlMixin.__init__(self, pv=pv, font="", fg=None, bg=None) + self.Bind(wx.EVT_SPINCTRL, self.OnSpin) + + @EpicsFunction + def _SetValue(self, value): + "set value" + wx.SpinCtrl.SetValue(self, self.pv.get()) + if hasattr(self, "pv"): # can be called before PV is assigned + self.pv.put(value) + + @EpicsFunction + def OnSpin(self, event=None): + "spin event handler" + self._SetValue(self.GetValue()) + +class PVButton(wx.Button, PVCtrlMixin): + """ A Button linked to a PV. When the button is pressed, a certain value + is written to the PV (useful for momentary PVs with HIGH= set.) + + """ + def __init__(self, parent, pv=None, pushValue=1, + disablePV=None, disableValue=1, **kw): + """ + pv = pv to write back to + pushValue = value to write when button is pressed + disablePV = read this PV in order to disable the button + disableValue = disable the button if/when the disablePV has this value + + """ + wx.Button.__init__(self, parent, **kw) + PVCtrlMixin.__init__(self, pv=pv, font="", fg=None, bg=None) + self.pushValue = pushValue + self.Bind(wx.EVT_BUTTON, self.OnPress) + if isinstance(disablePV, six.string_types): + disablePV = epics.get_pv(disablePV) + disablePV.connect() + self.disablePV = disablePV + self.disableValue = disableValue + if disablePV is not None: + ncback = len(self.disablePV.callbacks) + 1 + self.disablePV.add_callback(self._disableEvent, wid=self.GetId(), + cb_info=ncback) + self.maskedEnabled = True + + def Enable(self, value=None): + "enable button" + if value is not None: + self.maskedEnabled = value + self._UpdateEnabled() + + @EpicsFunction + def _UpdateEnabled(self): + "epics function, called by event handler" + enableValue = self.maskedEnabled + if self.disablePV is not None and \ + (self.disablePV.get() == self.disableValue): + enableValue = False + if self.pv is not None and (self.pv.get() == self.pushValue): + enableValue = False + wx.Button.Enable(self, enableValue) + + @DelayedEpicsCallback + def _disableEvent(self, **kw): + "disable event handler" + self._UpdateEnabled() + + def _SetValue(self, event): + "set value" + self._UpdateEnabled() + + @EpicsFunction + def OnPress(self, event): + "button press event handler" + self.pv.put(self.pushValue) + +class PVRadioButton(wx.RadioButton, PVCtrlMixin): + """A pvRadioButton is a radio button associated with a particular PV + and one particular value. + Suggested for use in a group where all radio buttons are + pvRadioButtons, and they all have a discrete value set. + + """ + def __init__(self, parent, pv=None, pvValue=None, **kw): + """ + pvValue = This value will be written to the PV when the radiobutton is + pushed, and the radiobutton will become select if/when the PV is set to + this value. + The value used is raw numeric, not "as string" + """ + wx.RadioButton.__init__(self, parent, **kw) + PVCtrlMixin.__init__(self, pv=pv, font="", fg=None, bg=None) + self.pvValue = pvValue + self.Bind(wx.EVT_RADIOBUTTON, self.OnPress) + + @EpicsFunction + def OnPress(self, event): + "button press event handler" + self.pv.put(self.pvValue) + + @EpicsFunction + def _SetValue(self, value): + "set value" + # uses raw PV val as is not string-converted + if value == self.pvValue or self.pv.get() == self.pvValue: + self.Value = True + + +class PVComboBox(wx.ComboBox, PVCtrlMixin): + """ A ComboBox linked to a PV. Both reads/writes the combo value on changes + + """ + def __init__(self, parent, pv=None, **kw): + wx.ComboBox.__init__(self, parent, **kw) + PVCtrlMixin.__init__(self, pv=pv, font="", fg=None, bg=None) + self.Bind(wx.EVT_TEXT, self.OnText) + + def _SetValue(self, value): + "set value" + if value != self.Value: + self.Value = value + + @EpicsFunction + def OnText(self, event): + "text event" + self.pv.put(self.Value) + +class PVEnumComboBox(PVComboBox): + """ A dropdown ComboBox linked to an enum PV, allows setting of enum values. + Both reads/writes the combo value on changes + """ + def __init__(self, parent, pv=None, **kw): + PVComboBox.__init__(self, parent, pv, style=wx.CB_DROPDOWN|wx.CB_READONLY, **kw) + + @DelayedEpicsCallback + def OnEpicsConnect(self, pvname=None, conn=None, pv=None): + PVComboBox.OnEpicsConnect(self, pvname, conn, pv) + if conn: + self.Clear() + for enum in self.pv.enum_strs: + self.Append(enum) + self.Value = self.pv.get(as_string=True) + +class PVToggleButton(wx.ToggleButton, PVCtrlMixin): + """A ToggleButton that can be attached to a bi or bo Epics record.""" + + def __init__(self, parent, pv=None, down=1, up_colour=None, + down_colour=None, **kwargs): + """ + Create a ToggleButton and attach it to a bi or bo record. + + Toggling the button will toggle the bi/bo record (and vice versa.) The + button label is the ONAM or ZNAM values of the record. Note the label + displays the opposite state of the bi/bo record, i.e., it shows what + will happen if the button is clicked. + + parent: Parent window of the ToggleButton. + pv: Process variable attached to the ToggleButton. A bi/bo record. + down: pv.value representing a down button. Default 1. + up_colour: Background colour of button when it is up. Default None. + down_colour: Background colour of button when it is down. Default None. + """ + wx.ToggleButton.__init__(self, parent, wx.ID_ANY, label='', **kwargs) + PVCtrlMixin.__init__(self, pv=pv) + + self.down = down + self.up_colour = up_colour + self.down_colour = down_colour + self.Bind(wx.EVT_TOGGLEBUTTON, self._onButton) + + @EpicsFunction + def _onButton(self, event=None): + "button event handler" + self.labels = self.pv.enum_strs + if self.GetValue(): + self.SetLabel(self.labels[0]) + self.pv.put(self.down == 1) + self.SetBackgroundColour(self.down_colour) + else: + self.SetLabel(self.labels[1]) + self.pv.put(self.down == 0) + self.SetBackgroundColour(self.up_colour) + + @EpicsFunction + def _SetValue(self, value): + "set value" + self.labels = self.pv.enum_strs + if value == self.labels[1]: + self.SetValue(self.down==1) + self.SetBackgroundColour(self.down_colour if self.down==1 \ + else self.up_colour) + self.SetLabel(self.labels[0]) + else: + self.SetValue(self.down==0) + self.SetBackgroundColour(self.down_colour if self.down==0 \ + else self.up_colour) + self.SetLabel(self.labels[1]) + +class PVStatusBar(wx.StatusBar, PVMixin): + """A status bar that displays a pv value + + To use in a wxFrame: + self.SetStatusBar(pvStatusBar(prent=self, pv=PV(...), style=...) + """ + + def __init__(self, parent=None, pv=None, **kwargs): + """ + Create a stsus bar that displays a pv value. + """ + wx.StatusBar.__init__(self, parent, wx.ID_ANY, **kwargs) + PVMixin.__init__(self, pv=pv) + + @EpicsFunction + def OnPVChange(self, str_value): + "called by PV callback" + self.SetStatusText(str_value) + + +class PVCollapsiblePane(wx.CollapsiblePane, PVCtrlMixin): + """A collapsible pane where the display on the collapsed line is set + from a PV value + """ + + 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 = { + epics.MINOR_ALARM : minor_alarm, + epics.MAJOR_ALARM : major_alarm, + epics.INVALID_ALARM : invalid_alarm } + if self.pv: + _SetValue(self.pv.value) + + def _SetValue(self, value): + if value: + self.SetLabel(value) diff --git a/scripts/Motor_Display.py b/scripts/Motor_Display.py new file mode 100755 index 0000000..24ed3ae --- /dev/null +++ b/scripts/Motor_Display.py @@ -0,0 +1,97 @@ +#!/usr/bin/python +# +# test the MotorPanel + +import wx +import sys +import time +import epics +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): + + wx.Frame.__init__(self, parent, wx.ID_ANY, '', + wx.DefaultPosition, wx.Size(-1,-1),**kwds) + self.SetTitle(" Epics Motors Page") + + 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() + 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.SetSizer(self.mainsizer) + self.mainsizer.Fit(self) + self.Refresh() + + def createMenus(self): + mbar = wx.MenuBar() + fmenu = wx.Menu() + 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) + 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) + + def onAbout(self, event): + dlg = wx.MessageDialog(self, "WX Motor is was written by \n" + "Matt Newville \n" + "About Me", wx.OK | wx.ICON_INFORMATION) + dlg.ShowModal() + dlg.Destroy() + + def onMotorChoice(self,event,motor=None): + self.motor1.SelectMotor(self.motors[event.GetString()]) + + def onClose(self, event): + finalize_epics() + self.Destroy() + +if __name__ == '__main__': + 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() + print("App ", time.ctime()) + + app.MainLoop() diff --git a/scripts/README b/scripts/README new file mode 100644 index 0000000..39b01cd --- /dev/null +++ b/scripts/README @@ -0,0 +1,30 @@ +These programs demonstrate some simple uses of the epics module. For more +complex examples, including GUI applications built with wxWidget displays, +see the pyepics/epicsapp repository at + https://github.com/pyepics/epicsapps + +== Motor_Display.py: +This provides a simple GUI interface to Epics Motors. Multiple motors can +be specified. These will be shown as rows in the main page, much like an +MEDM screen, only nicer. Each motor will be fully connected to the Epics +PVs of the Epics Motor Record. A ``more`` button will bring up a separate +window with more details of the Motor Record. + + ~> python Motor_Display.py XXX:m1 XXX:m2 XXX:m3 + +This program requires the wxPython package. + +== caget.py +This is a simple emulation of the the caget command. + + ~> python caget.py XXX.VAL + +== save_restore.py +This is a simple commandline tool for saving/restoring PV values + +For full options, run: + ~> ./save_restore.py -h + +Simple uses: + ~> ./save_restore.py -d --save /tmp/my_pvs.req /tmp/my_save.sav + ~> ./save_restore.py -d --save /tmp/my_save.sav diff --git a/scripts/caget.py b/scripts/caget.py new file mode 100644 index 0000000..202e4f9 --- /dev/null +++ b/scripts/caget.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python + +import epics +import time +import sys +import getopt + +def show_usage(): + s = """Usage caget.py [options] ... + -h: show this message + + + """ + print s + sys.exit() + +opts, args = getopt.getopt(sys.argv[1:], "htansd:#:e:f:g:w:", + ["help", "terse","wide","number","data=",]) + +wait_time = 1.0 + +arr_num = None +format = None +terse = False +use_ts = False +dbr_type = None +enum_as_num = False + +for (k,v) in opts: + if k in ("-h", "--help"): show_usage() + elif k == '-t': terse = True + elif k == '-a': use_ts = True + elif k == '-n': enum_as_num = True + elif k == '-d': dbr_type = int(v) + elif k == '-#': arr_num = int(v) + elif k == '-w': wait_time= float(v) + elif k == '-e': format = '%%.%ie' % int(v) + elif k == '-f': format = '%%.%if' % int(v) + elif k == '-g': format = '%%.%ig' % int(v) + elif k == '-s': format = 'STRING' + +for pvname in args: + form='native' + if use_ts: form = 'time' + pv = epics.PV(pvname,form=form) + + # pv connection + t0 = time.time() + pv.connect() + while time.time()-t0 < wait_time: + if pv.connected: break + epics.poll() + if not pv.connected: + print "%s:: not connected" % pvname + ox = pv.get_ctrlvars() + epics.poll() + + + value = pv.get(as_string=format=='STRING') + + if pv.type == 'enum' and not enum_as_num: + enum_strs = pv.enum_strs + value = enum_strs[pv.get()] + + outstring = '' + if not terse: outstring = pvname + + if use_ts: + ts = epics.dbr.EPICS2UNIX_EPOCH + pv._args['timestamp'] + outstring = "%s %s" %(outstring,time.ctime(ts)) + print ts + print outstring, value + 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/scripts/save_restore.py b/scripts/save_restore.py new file mode 100755 index 0000000..46561ad --- /dev/null +++ b/scripts/save_restore.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python +""" + +Simple script that demonstrates how to use the epics.autosave module to +save & restore PV values using request files and savefiles, like with synApps +autosave (only via CA.) + +Run ./save_restore.py -h for a full usage summary. + +""" + +from optparse import * +from epics.autosave import * + +def main(): + (options, args) = get_validate_args() + methods = { "restore" : do_restore, "save" : do_save } + methods[options.mode](options.debug, args) + +def do_restore(debug, args): + print "Restoring PVs from save file %s..." % (args[0]) + if not restore_pvs(args[0], debug): + sys.exit(1) + +def do_save(debug, args): + print "Saving PVs in %s to save file %s..." % (args[0], args[1]) + try: + save_pvs(args[0], args[1], debug) + except Exception,e: + sys.exit("Failed to save pvs: %s" % e) + +def get_validate_args(): + """ + Parse and validate command-line arguments + + """ + usage = "usage: %prog [options] [restore-file] save-file" + parser = OptionParser(usage=usage, description="Save & restore EPICS pvs via Channel Access. Options --save & --restore are not required, if they are ommitted then the mode will be inferred from the number of arguments.") + + group = OptionGroup(parser, "Mode") + group.add_option("-r", "--restore", action="store_const", const="restore", dest="mode", + help="Restore PV values saved in ") + group.add_option("-s", "--save", action="store_const", const="save", dest="mode", + help="Save PV values in named in to ") + parser.add_option_group(group) + + group = OptionGroup(parser, "Debug Options") + group.add_option("-d", "--debug", dest="debug", action="store_true", + help="Print each PV as it is saved/restored") + parser.add_option_group(group) + + (options, args) = parser.parse_args() + + if options.mode is None and len(args) == 2: + options.mode = "save" + else: + options.mode = "restore" + + if options.mode == "save": + if len(args) != 2: + parser.error("Saving pvs requires two arguments - restore-file save-file") + elif options.mode == "restore": + if len(args) != 1: + parser.error("Restoring pvs requires one argument - save-file") + else: + parser.error("Unexpected command line arguments") + return (options, args) + +if __name__ == "__main__": + main() + diff --git a/scripts/wxProbe.py b/scripts/wxProbe.py new file mode 100755 index 0000000..bb21517 --- /dev/null +++ b/scripts/wxProbe.py @@ -0,0 +1,118 @@ +#!/usr/bin/python +# +# simple PV Probe application + +import wx +import sys +import epics +from epics.wx import EpicsFunction, DelayedEpicsCallback + +class PVDisplay(wx.Frame): + def __init__(self, pvname, parent=None, **kwds): + wx.Frame.__init__(self, parent, wx.ID_ANY, '', + wx.DefaultPosition, wx.Size(-1,-1),**kwds) + self.SetFont(wx.Font(11,wx.SWISS,wx.NORMAL,wx.BOLD,False)) + self.pvname = pvname + + self.SetTitle("%s" % pvname) + + self.sizer = wx.GridBagSizer(3, 2) + panel = wx.Panel(self) + name = wx.StaticText(panel, label=pvname, size=(120, -1)) + self.val = wx.StaticText(panel, label='unconnected', size=(200, -1)) + self.info = wx.StaticText(panel, label='-- ' , size=(400,300)) + self.info.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD,False)) + + self.sizer.Add(wx.StaticText(panel, label='PV: ', size=(60, -1)), + (0, 0), (1, 1), wx.EXPAND, 1) + self.sizer.Add(wx.StaticText(panel, label='Value: ', size=(60, -1)), + (1, 0), (1, 1), wx.EXPAND, 1) + self.sizer.Add(wx.StaticText(panel, label='Info: ', size=(60, -1)), + (2, 0), (1, 1), wx.EXPAND, 1) + self.sizer.Add(name, (0, 1), (1, 1), wx.EXPAND, 1) + self.sizer.Add(self.val, (1, 1), (1, 1), wx.ALIGN_RIGHT|wx.EXPAND, 1) + self.sizer.Add(self.info, (2, 1), (2, 2), wx.ALIGN_LEFT|wx.EXPAND, 1) + + panel.SetSizer(self.sizer) + + self.needs_info = None + self.timer = wx.Timer(self) + self.Bind(wx.EVT_TIMER, self.onTimer, self.timer) + + self.s1 = wx.BoxSizer(wx.VERTICAL) + self.s1.Add(panel, 1, wx.EXPAND, 2) + self.s1.Fit(self) + self.Refresh() + self.connect_pv() + + @EpicsFunction + def connect_pv(self): + self.pv = epics.PV(self.pvname, connection_callback=self.onConnect, + callback=self.onPV_value) + + @EpicsFunction + def onTimer(self, evt): + if self.need_info and self.pv.connected: + self.info.SetLabel(self.pv.info) + self.timer.Stop() + self.needs_info = False + + @DelayedEpicsCallback + def onConnect(self, **kws): + self.need_info = True + self.timer.Start(25) + + @DelayedEpicsCallback + def onPV_value(self, name=None, char_value=None, **kws): + if len(char_value) > 90: + char_value = char_value[:90] + self.val.SetLabel(' %s' % char_value) + self.need_info = True + self.timer.Start(25) + + +class NameCtrl(wx.TextCtrl): + def __init__(self, panel, value='', action=None, **kws): + self.action = action + wx.TextCtrl.__init__(self, panel, wx.ID_ANY, value='', + style=wx.TE_PROCESS_ENTER, **kws) + self.Bind(wx.EVT_CHAR, self.onChar) + + def onChar(self, event): + if event.GetKeyCode() == wx.WXK_RETURN and \ + self.action is not None: + self.action(wx.TextCtrl.GetValue(self).strip()) + event.Skip() + + +class ProbeFrame(wx.Frame): + def __init__(self, parent=None, **kwds): + + wx.Frame.__init__(self, parent, wx.ID_ANY, '', + wx.DefaultPosition, wx.Size(-1,-1),**kwds) + self.SetTitle("Connect to Epics Records:") + + self.SetFont(wx.Font(11,wx.SWISS,wx.NORMAL,wx.BOLD,False)) + + sizer = wx.BoxSizer(wx.HORIZONTAL) + panel = wx.Panel(self) + label = wx.StaticText(panel, label='PV Name:') + self.pvname = NameCtrl(panel, value='', size=(175,-1), + action=self.onName) + + sizer.Add(label, 0, wx.ALIGN_LEFT, 1) + sizer.Add(self.pvname, 1, wx.EXPAND, 1) + panel.SetSizer(sizer) + sizer.Fit(panel) + s = wx.BoxSizer(wx.VERTICAL) + s.Add(panel) + s.Fit(self) + self.Refresh() + + def onName(self, value, wid=None, **kws): + PVDisplay(value, parent=self).Show() + +if __name__ == '__main__': + app = wx.App(redirect=False) + ProbeFrame().Show() + app.MainLoop() diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..dc74e9e --- /dev/null +++ b/setup.cfg @@ -0,0 +1,11 @@ +[versioneer] +vcs = git +versionfile_source = epics/_version.py +versionfile_build = epics/_version.py +tag_prefix = +parentdir_prefix = pyepics- + +[egg_info] +tag_build = +tag_date = 0 + diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..7ca751f --- /dev/null +++ b/setup.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python +# from distutils.core import setup +from setuptools import setup + +import os +import sys +import epics +import shutil + +import versioneer + +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. + +In addition, the library includes convenience classes to define +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. +''' + +# +no_libca="""******************************************************* +*** WARNING - WARNING - WARNING - WARNING - WARNING *** + + Could not find CA dynamic library! + +A dynamic library (libca.so or libca.dylib) for EPICS CA +must be found in order for EPICS calls to work properly. + +Please read the INSTALL inststructions, and fix this +problem before tyring to use the epics package. +******************************************************* +""" + +nolibca = os.environ.get('NOLIBCA', None) +if nolibca is None: + pkg_data = {'epics.clibs': ['darwin64/*', 'linux64/*', 'linux32/*', + 'linuxarm/*', 'win32/*', 'win64/*']} +else: + pkg_data = dict() + +PY_MAJOR, PY_MINOR = sys.version_info[:2] +if PY_MAJOR == 2 and PY_MINOR < 6: + shutil.copy(pjoin('epics', 'utils3.py'), + pjoin('epics', 'utils3_save_py.txt')) + shutil.copy(pjoin('epics', 'utils2.py'), + pjoin('epics', 'utils3.py')) + +setup(name = 'pyepics', + version = versioneer.get_version(), + cmdclass = versioneer.get_cmdclass(), + author = 'Matthew Newville', + author_email = 'newville@cars.uchicago.edu', + url = 'http://pyepics.github.io/pyepics/', + download_url = 'http://pyepics.github.io/pyepics/', + license = 'Epics Open License', + description = "Epics Channel Access for Python", + long_description = long_desc, + platforms = ['Windows', 'Linux', 'Mac OS X'], + classifiers = ['Intended Audience :: Science/Research', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Topic :: Scientific/Engineering'], + packages = ['epics','epics.wx','epics.devices', 'epics.compat', + 'epics.autosave', 'epics.clibs'], + package_data = pkg_data, + ) + +try: + libca = epics.ca.find_libca() +except: + sys.stdout.write("%s" % no_libca) diff --git a/tests/AutoSaveSimple.req b/tests/AutoSaveSimple.req new file mode 100644 index 0000000..1b0dcf8 --- /dev/null +++ b/tests/AutoSaveSimple.req @@ -0,0 +1,10 @@ +$(P)ao1.VAL +$(P)ao1.EGU +$(P)ao1.PREC +$(P)ai1.VAL +$(P)ai1.EGU +$(P)ai1.PREC +$(P)long1.VAL +$(P)str1.VAL +$(P)char128.VAL +$(P)double128.VAL diff --git a/tests/AutoSaveTest.req b/tests/AutoSaveTest.req new file mode 100644 index 0000000..226e49b --- /dev/null +++ b/tests/AutoSaveTest.req @@ -0,0 +1,2 @@ +file "AutoSaveSimple.req", P=Py: + diff --git a/tests/Setup/pydebug.db b/tests/Setup/pydebug.db new file mode 100644 index 0000000..0718270 --- /dev/null +++ b/tests/Setup/pydebug.db @@ -0,0 +1,274 @@ +## +## basic data types for debugging +record(mbbo,"$(P)mbbo1") { + field(DESC,"mbbo") + field(ZRVL,"0") + field(ZRST,"Stop") + field(ONVL,"1") + field(ONST,"Start") + field(TWVL,"2") + field(TWST,"Pause") + field(THVL,"3") + field(THST,"Resume") +} +record(mbbo,"$(P)mbbo2") { + field(DESC,"mbbo") + field(ZRVL,"0") + field(ZRST,"Stop") + field(ONVL,"1") + field(ONST,"Start") + field(TWVL,"2") + field(TWST,"Pause") + field(THVL,"3") + field(THST,"Resume") +} + +record(mbbo,"$(P)pause") { + field(DESC,"mbbo") + field(ZRVL,"0") + field(ZRST,"Not Paused") + field(ONVL,"1") + field(ONST,"Paused") +} + +record(waveform,"$(P)char128") { + field(DTYP,"Soft Channel") + field(DESC,"short char waveform") + field(NELM,"128") + field(FTVL,"UCHAR") +} + +record(waveform,"$(P)char256") { + field(DTYP,"Soft Channel") + field(DESC,"short char waveform") + field(NELM,"256") + field(FTVL,"UCHAR") +} + +record(waveform,"$(P)char2k") { + field(DTYP,"Soft Channel") + field(DESC,"medium char waveform") + field(NELM,"2048") + field(FTVL,"UCHAR") +} + +record(waveform,"$(P)char64k") { + field(DESC,"long char waveform") + field(DTYP,"Soft Channel") + field(NELM,"65536") + field(FTVL,"UCHAR") +} + +record(waveform,"$(P)double128") { + field(DTYP,"Soft Channel") + field(DESC,"short double waveform") + field(NELM,"128") + field(FTVL,"DOUBLE") +} + +record(waveform,"$(P)double2k") { + field(DTYP,"Soft Channel") + field(DESC,"medium double waveform") + field(NELM,"2048") + field(FTVL,"DOUBLE") +} + +record(waveform,"$(P)double64k") { + field(DESC,"long double waveform") + field(DTYP,"Soft Channel") + field(NELM,"65536") + field(FTVL,"DOUBLE") +} + +record(waveform,"$(P)long128") { + field(DTYP,"Soft Channel") + field(DESC,"short long waveform") + field(NELM,"128") + field(FTVL,"LONG") +} + +record(waveform,"$(P)long2k") { + field(DTYP,"Soft Channel") + field(DESC,"medium long waveform") + field(NELM,"2048") + field(FTVL,"LONG") +} + +record(waveform,"$(P)long64k") { + field(DESC,"long long waveform") + field(DTYP,"Soft Channel") + field(NELM,"65536") + field(FTVL,"LONG") +} + +record(waveform,"$(P)string128") { + field(DTYP,"Soft Channel") + field(DESC,"short string waveform") + field(NELM,"128") + field(FTVL,"STRING") +} + +record(waveform,"$(P)string2k") { + field(DTYP,"Soft Channel") + field(DESC,"medium string waveform") + field(NELM,"2048") + field(FTVL,"STRING") +} + +record(waveform,"$(P)string64k") { + field(DESC,"long string waveform") + field(DTYP,"Soft Channel") + field(NELM,"65536") + field(FTVL,"STRING") +} + +record(longin,"$(P)long1") { + field(DESC,"longin") + field(DESC, "Soft Channel") + field(VAL, "123456") +} + +record(longout,"$(P)long2") { + field(DESC,"longout") + field(DTYP, "Soft Channel") + field(VAL, "543210") +} + +record(longout,"$(P)long3") { + field(DESC,"longout") + field(DTYP, "Soft Channel") + field(VAL, "543210") +} + +record(longout,"$(P)long4") { + field(DESC,"longout") + field(DTYP, "Soft Channel") + field(VAL, "543210") +} + + +record(stringin,"$(P)str1") { + field(DESC,"stringin") + field(DTYP, "Soft Channel") + field(VAL, "s") +} + +record(stringout,"$(P)str2") { + field(DESC,"stringout") + field(DTYP, "Soft Channel") + field(VAL, "") +} + +record(ao,"$(P)ao1") { + field(DESC, "ao") + field(VAL, "1") +} + +record(ai,"$(P)ai1") { + field(DESC, "ai") + field(VAL, "1") +} + +record(ao,"$(P)ao2") { + field(DESC, "ao") + field(VAL, "1") +} + +record(ao,"$(P)ao3") { + field(DESC, "ao") + field(VAL, "1") +} + +record(ao,"$(P)ao4") { + field(DESC, "ao") + field(VAL, "1") +} + +record(bo,"$(P)bo1") { + field(DESC, "bo") + field(VAL, "1") +} + +record(bi,"$(P)bi1") { + field(DESC, "bi") + field(VAL, "1") +} + +## Subarrays Copied from a Tech-Talk conversation, March 2011 + +record(subArray, "$(P)subArr1") { + field(DESC, "sub array 1") + field(NELM, "16") + field(INP, "$(P)wave_test.VAL") + field(INDX, "0") + field(FTVL, "DOUBLE") + field(MALM, "64") +} + +record(subArray, "$(P)subArr2") { + field(DESC, "sub array 2") + field(NELM, "16") + field(INP, "$(P)wave_test.VAL") + field(INDX, "16") + field(FTVL, "DOUBLE") + field(MALM, "64") +} + +record(subArray, "$(P)subArr3") { + field(DESC, "sub array 3") + field(NELM, "16") + field(INP, "$(P)wave_test.VAL") + field(INDX, "32") + field(FTVL, "DOUBLE") + field(MALM, "64") +} + +record(subArray, "$(P)subArr4") { + field(DESC, "sub array 4") + field(NELM, "16") + field(INP, "$(P)wave_test.VAL") + field(INDX, "48") + field(FTVL, "DOUBLE") + field(MALM, "64") +} + +record(subArray, "$(P)ZeroLenSubArr1") { + field(DESC, "zero length subarray") + field(NELM, "0") + field(INP, "$(P)wave_test.VAL") + field(INDX, "10") + field(FTVL, "DOUBLE") + field(MALM, "64") +} + +record(fanout, "$(P)mylinker") { + field(FLNK, "$(P)subArr1") + field(LNK1, "$(P)subArr3") + field(LNK2, "$(P)subArr2") + field(LNK3, "$(P)subArr4") + field(LNK4, "$(P)ZeroLenSubArr1") +} + +record(waveform, "$(P)wave_test") { + field(NELM, "64") + field(FTVL, "DOUBLE") + field(EGU, "Counts") + field(SCAN, "Passive") + field(FLNK, "$(P)mylinker") +} + + +record(bi,"$(P)xbi") { + field(DESC, "bi") + field(VAL, "1") + field(FLNK, "$(P)xbo") +} + + +record(bi,"$(P)xbo") { + field(DESC, "bo") + field(VAL, "0") +} + + + diff --git a/tests/Setup/simulator.py b/tests/Setup/simulator.py new file mode 100644 index 0000000..e8bf626 --- /dev/null +++ b/tests/Setup/simulator.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python +# +# test simulator for testing pyepics. +# +# this script changes values definied in the pydebug.db, which all +# the tests scripts use. This script must be running (somewhere) +# for many of the tests (callbacks, etc) to work. + +# +import epics +import time +import random +import numpy + +prefix = 'Py:' + +global NEEDS_INIT + +NEEDS_INIT = True +SLEEP_TIME = 0.10 + +def onConnect(pvname=None, conn=None, **kws): + global NEEDS_INIT + NEEDS_INIT = conn + + +def make_pvs(*args, **kwds): + # print "Make PVS ' ", prefix, args + # print [("%s%s" % (prefix, name)) for name in args] + pvlist = [epics.PV("%s%s" % (prefix, name)) for name in args] + for pv in pvlist: + pv.connect() + pv.connection_callbacks.append(onConnect) + return pvlist + +mbbos = make_pvs("mbbo1","mbbo2") +pause_pv = make_pvs("pause",)[0] +longs = make_pvs("long1", "long2", "long3", "long4") +strs = make_pvs("str1", "str2") +analogs = make_pvs("ao1", "ai1", "ao2", "ao3") +binaries = make_pvs("bo1", "bi1") + +char_waves = make_pvs("char128", "char256", "char2k", "char64k") +double_waves = make_pvs("double128", "double2k", "double64k") +long_waves = make_pvs("long128", "long2k", "long64k") +str_waves = make_pvs("string128", "string2k", "string64k") + +subarrays = make_pvs("subArr1", "subArr2", "subArr3", "subArr4" ) +subarray_driver = make_pvs("wave_test",)[0] + + +def initialize_data(): + subarray_driver.put(numpy.arange(64)/12.0) + + for p in mbbos: + p.put(1) + + for i, p in enumerate(longs): p.put((i+1)) + + for i, p in enumerate(strs): p.put("String %s" % (i+1)) + + for i, p in enumerate(binaries): p.put((i+1)) + + for i, p in enumerate(analogs): p.put((i+1)*1.7135000 ) + + epics.caput('Py:ao1.EGU', 'microns') + epics.caput('Py:ao1.PREC', 4) + epics.caput('Py:ai1.PREC', 2) + epics.caput('Py:ao2.PREC', 3) + + + + char_waves[0].put([60+random.randrange(30) for i in range(128)]) + char_waves[1].put([random.randrange(256) for i in range(256)]) + char_waves[2].put([random.randrange(256) for i in range(2048)]) + char_waves[3].put([random.randrange(256) for i in range(65536)]) + + + long_waves[0].put([i+random.randrange(2) for i in range(128)]) + long_waves[1].put([i+random.randrange(128) for i in range(2048)]) + long_waves[2].put([i for i in range(65536)]) + + double_waves[0].put([i+random.randrange(2) for i in range(128)]) + double_waves[1].put([random.random() for i in range(2048)]) + double_waves[2].put([random.random() for i in range(65536)]) + + pause_pv.put(0) + str_waves[0].put([" String %i" % (i+1) for i in range(128)]) + print( 'Data initialized') + +text = '''line 1 +this is line 2 +and line 3 +here is another line +this is the 5th line +line 6 +line 7 +line 8 +line 9 +line 10 +line 11 +'''.split('\n') + +start_time = time.time() +count = 0 +long_update = 0 +lcount =1 + +while True: + if NEEDS_INIT: + initialize_data() + time.sleep(SLEEP_TIME) + NEEDS_INIT = False + + time.sleep(SLEEP_TIME) + + count = count + 1 + if count == 3: print( 'running') + if count > 99999999: count = 1 + + t0 = time.time() + if pause_pv.get() == 1: + # pause for up to 120 seconds if pause was selected + t0 = time.time() + while time.time()-t0 < 120: + time.sleep(SLEEP_TIME) + if pause_pv.get() == 0: + break + pause_pv.put(0) + noise = numpy.random.normal + + analogs[0].put( 100*(random.random()-0.5)) + analogs[1].put( 76.54321*(time.time()-start_time)) + analogs[2].put( 0.3*numpy.sin(time.time() / 2.302) + noise(scale=0.4) ) + char_waves[0].put([45+random.randrange(64) for i in range(128)]) + + if count % 3 == 0: + analogs[3].put( numpy.exp((max(0.001, noise(scale=0.03) + + numpy.sqrt((count/16.0) % 87.))))) + + long_waves[1].put([i+random.randrange(128) for i in range(2048)]) + str_waves[0].put(["Str%i_%.3f" % (i+1, 100*random.random()) for i in range(128)]) + + if t0-long_update >= 1.0: + long_update=t0 + lcount = (lcount + 1) % 10 + longs[0].put(lcount) + char_waves[1].put(text[lcount]) + double_waves[2].put([random.random() for i in range(65536)]) + double_waves[1].put([random.random() for i in range(2048)]) diff --git a/tests/Setup/st.cmd b/tests/Setup/st.cmd new file mode 100755 index 0000000..15f71c6 --- /dev/null +++ b/tests/Setup/st.cmd @@ -0,0 +1,26 @@ +errlogInit(5000) +< envPaths +dbLoadDatabase("../../dbd/CARSLinux.dbd") +CARSLinux_registerRecordDeviceDriver(pdbbase) + +### Scan-support software +# crate-resident scan. This executes 1D, 2D, 3D, and 4D scans, and caches +# 1D data, but it doesn't store anything to disk. (You need the data catcher +# or the equivalent for that.) This database is configured to use the +# "alldone" database (above) to figure out when motors have stopped moving +# and it's time to trigger detectors. +dbLoadRecords("Db/scan.db", "P=Py:,MAXPTS1=2000,MAXPTS2=200,MAXPTS3=20,MAXPTS4=10,MAXPTSH=10") + +# Free-standing user string/number calculations (sCalcout records) +dbLoadRecords("Db/userStringCalcs10.db", "P=Py:") + +# Free-standing user transforms (transform records) +dbLoadRecords("Db/userTransforms10.db", "P=Py:") + +# dbLoadRecords("$(ASYN)/db/asynRecord.db", "P=13XRM:,R=asyn1,PORT=XPS1,ADDR=0,IMAX=256,OMAX=256") + +# test databases +dbLoadRecords("Db/pydebug.db", "P=Py:") + +iocInit + diff --git a/tests/alarm.py b/tests/alarm.py new file mode 100644 index 0000000..15e8464 --- /dev/null +++ b/tests/alarm.py @@ -0,0 +1,35 @@ +import sys +import time +import epics +import pvnames + +pvn = pvnames.alarm_pv +pvn = pvnames.alarm_comp +pvn = pvnames.alarm_trippoint + +def alarmHandler(pvname=None,value=None,char_value=None, + comparison=None,trip_point=None,**kw): + sys.stdout.write( 'Alarm! %s at %s ! \n' %( pvname, time.ctime())) + sys.stdout.write( 'Alarm Comparison =%s \n' %( comparison)) + sys.stdout.write( 'Alarm TripPoint =%s \n' %( repr(trip_point))) + sys.stdout.write( 'Current Value =%s \n' %(char_value)) + +epics.Alarm(pvname = pvnames.alarm_pv, + comparison = pvnames.alarm_comp, + trip_point =pvnames.alarm_trippoint, + callback = alarmHandler, + alert_delay=5.0) + + +t0 = time.time() + +sys.stdout.write('Waiting for pv %s to change! \n' % pvnames.alarm_pv) +sys.stdout.write('Alarm settings: comp=%s, trip_point=%s\n' % (pvnames.alarm_comp, + pvnames.alarm_trippoint)) +sys.stdout.write('You may have to make this happen!!\n') + +while time.time()-t0 < 30: + try: + epics.ca.poll() + except KeyboardInterrupt: + break diff --git a/tests/autosave_test.py b/tests/autosave_test.py new file mode 100644 index 0000000..904b767 --- /dev/null +++ b/tests/autosave_test.py @@ -0,0 +1,12 @@ +import epics.autosave +import time +epics.autosave.save_pvs('AutoSaveTest.req', 'tmp.sav') + +time.sleep(0.5) +f = open('tmp.sav','r') + +data = f.read() + +if len(data) > 50: + print("AutoSave worked... data written to file 'tmp.sav'") + diff --git a/tests/ca_connection_callback.py b/tests/ca_connection_callback.py new file mode 100644 index 0000000..fc01dee --- /dev/null +++ b/tests/ca_connection_callback.py @@ -0,0 +1,21 @@ +# +# example of using a connection callback that will be called +# for any change in connection status + +import epics +import time +import pvnames + + + +import sys +write = sys.stdout.write +def onConnectionChange(pvname=None, **kws): + write('ca connection status changed: %s %s\n' % ( pvname, repr(kws))) + +chid = epics.ca.create_channel(pvnames.long_pv, callback=onConnectionChange) + +write('Now, restart simulation IOC -- waiting, watching values and connection changes:\n') +t0 = time.time() +while time.time()-t0 < 15: + time.sleep(0.001) diff --git a/tests/ca_fastconn.py b/tests/ca_fastconn.py new file mode 100644 index 0000000..a3a685b --- /dev/null +++ b/tests/ca_fastconn.py @@ -0,0 +1,71 @@ +from epics import ca, dbr +import time +import debugtime +try: + from collections import OrderedDict +except: + from ordereddict import OrderedDict +dt = debugtime.debugtime() +def add(x): + print(x) + dt.add(x) + +add('test of fast connection to many PVs') +pvnames = [] + +results = OrderedDict() + +MAX_PVS = 12500 + +for line in open('fastconn_pvlist.txt','r').readlines(): + if not line.startswith('#'): + pvnames.append(line.strip()) + +if MAX_PVS is not None: + pvnames = pvnames[:MAX_PVS] + + +add('Read PV list: Will connect to %i PVs' % len(pvnames)) +libca = ca.initialize_libca() + +for name in pvnames: + chid = ca.create_channel(name, connect=False, auto_cb=False) + results[name] = {'chid': chid} + +time.sleep(0.001) + +add("created PVs with ca_create_channel") + +for name in pvnames: + ca.connect_channel(results[name]['chid']) + +time.sleep(0.001) + +add("connected to PVs with connect_channel") + +ca.pend_event(1.e-2) + +for name in pvnames: + chid = results[name]['chid'] + val = ca.get(chid, wait=False) + results[name]['value'] = val + +add("did ca.get(wait=False)") +ca.poll(2.e-3, 1.0) +add("ca.poll() complete") + +for name in pvnames: + results[name]['value'] = ca.get_complete(results[name]['chid']) + +add("ca.get_complete() for all PVs") + +f = open('fastconn_pvdata.sav', 'w') +for name, val in results.items(): + f.write("%s %s\n" % (name.strip(), val['value'])) +f.close() +add("wrote PV values to disk") + +dt.show() + +time.sleep(0.01) +ca.poll() diff --git a/tests/ca_subscribe.py b/tests/ca_subscribe.py new file mode 100644 index 0000000..9a4a2be --- /dev/null +++ b/tests/ca_subscribe.py @@ -0,0 +1,20 @@ +from epics import ca + +import time +import sys +import pvnames +mypv = pvnames.updating_pv1 + +def onChanges(pvname=None, value=None, **kw): + sys.stdout.write( 'New Value: %s value=%s, kw=%s\n' %( pvname, str(value), repr(kw))) + sys.stdout.flush() + +chid = ca.create_channel(mypv) +eventID = ca.create_subscription(chid, callback=onChanges) + +t0 = time.time() +while time.time()-t0 < 15.0: + time.sleep(0.001) + + + diff --git a/tests/ca_subscribe2.py b/tests/ca_subscribe2.py new file mode 100644 index 0000000..167884e --- /dev/null +++ b/tests/ca_subscribe2.py @@ -0,0 +1,28 @@ +import time +import sys + +from epics import ca + +import pvnames + +pvname = pvnames.updating_pv1 + +def wait(step=0.1, maxtime=30): + t0 = time.time() + while time.time()-t0 < maxtime: + time.sleep(step) + + +def setup_callback(pvname): + def my_cb(pvname=None, value=None, **kw): + sys.stdout.write( 'get: %s value=%s, kw=%s\n' %( pvname, str(value), repr(kw))) + sys.stdout.flush() + + chid = ca.create_channel(pvname) + return ca.create_subscription(chid, callback=my_cb) + +cb_ref = setup_callback(pvname) + +wait() + + diff --git a/tests/ca_type_conversion.py b/tests/ca_type_conversion.py new file mode 100644 index 0000000..0f6006a --- /dev/null +++ b/tests/ca_type_conversion.py @@ -0,0 +1,76 @@ +from __future__ import print_function + +import sys +import time +import epics +import pvnames + +pvlist = ( + pvnames.str_pv, + pvnames.int_pv, + pvnames.float_pv, + pvnames.enum_pv, + pvnames.char_arr_pv, + pvnames.long_pv, + pvnames.long_arr_pv, + pvnames.double_pv, + pvnames.double_arr_pv, + pvnames.string_arr_pv, + ) + + +def RunTest(pvlist, use_preempt=True, maxlen=16384, + use_numpy=True, use_time=False, use_ctrl=False): + msg= ">>>Run Test: %i pvs, numpy=%s, time=%s, ctrl=%s, preempt=%s" + print( msg % (len(pvlist), use_numpy, use_time, use_ctrl, use_preempt)) + + epics.ca.HAS_NUMPY = epics.ca.HAS_NUMPY and use_numpy + epics.ca.PREEMPTIVE_CALLBACK = use_preempt + epics.ca.AUTOMONITOR_MAXLENGTH = maxlen + chids= [] + epics.ca.initialize_libca() + + def onConnect(pvname=None, **kw): + print(' on Connect %s %s' % (pvname, repr(kw))) + + def onChanges(chid=None, value=None, **kw): + print(' on Change chid=%i value=%s' % (int(chid), repr(value))) + + for pvname in pvlist: + chid = epics.ca.create_channel(pvname, callback=onConnect) + epics.ca.connect_channel(chid) + eventID = epics.ca.create_subscription(chid, callback=onChanges) + chids.append((chid, eventID)) + epics.poll(evt=0.025, iot=5.0) + epics.poll(evt=0.025, iot=10.0) + time.sleep(0.05) + for (chid, eventID) in chids: + print('=== %s chid=%s' % (epics.ca.name(chid), repr(chid))) + time.sleep(0.005) + ntype = epics.ca.promote_type(chid, use_ctrl=use_ctrl, + use_time=use_time) + val = epics.ca.get(chid, ftype=ntype) + cval = epics.ca.get(chid, as_string=True) + if epics.ca.element_count(chid) > 10: + val = val[:10] + time.sleep(0.005) + print("%i %s %s" % (ntype, epics.dbr.Name(ntype).lower(), cval)) + time.sleep(0.5) + print('----- finalizing CA') + epics.ca.finalize_libca() + time.sleep(0.05) + +for use_preempt in (True, False): + for use_numpy in (True, False): + for use_time, use_ctrl in ((False, False), + (True, False), + (False, True), + ): + print("==== NUMPY/TIME/CTRL ", use_numpy, use_time, use_ctrl) + RunTest(pvlist, + use_preempt=use_preempt, + use_numpy=use_numpy, + use_time=use_time, + use_ctrl=use_ctrl) + # sys.exit() + diff --git a/tests/ca_unittest.py b/tests/ca_unittest.py new file mode 100644 index 0000000..a7b1bef --- /dev/null +++ b/tests/ca_unittest.py @@ -0,0 +1,451 @@ +#!/usr/bin/env python +# test expression parsing + +import os +import sys +import time +import unittest +import numpy +import ctypes +from contextlib import contextmanager +from epics import ca, dbr, caput + +import pvnames + +def _ca_connect(chid,timeout=5.0): + n = 0 + t0 = time.time() + conn = 2==ca.state(chid) + while (not conn) and (time.time()-t0 < timeout): + ca.poll(1.e-6,1.e-4) + conn = 2==ca.state(chid) + n += 1 + return conn, time.time()-t0, n + +def write(msg): + sys.stdout.write('%s\n'% msg) + sys.stdout.flush() + +CONN_DAT ={} +CHANGE_DAT = {} + +def onConnect(pvname=None, conn=None, chid=None, **kws): + write(' /// Connection status changed: %s %s' % (pvname, repr(kws))) + global CONN_DAT + CONN_DAT[pvname] = conn + +def onChanges(pvname=None, value=None, **kws): + write( '/// New Value: %s value=%s, kw=%s' %( pvname, str(value), repr(kws))) + global CHANGE_DAT + CHANGE_DAT[pvname] = value + +@contextmanager +def no_simulator_updates(): + '''Context manager which pauses and resumes simulator PV updating''' + try: + caput(pvnames.pause_pv, 1) + yield + finally: + caput(pvnames.pause_pv, 0) + +class CA_BasicTests(unittest.TestCase): + + def setUp(self): + write('Starting %s...' % self.id().split(".")[-1]) + def tearDown(self): + write('Completed %s...\n' % self.id().split(".")[-1]) + + def testA_CreateChid(self): + write('Simple Test: create chid') + chid = ca.create_channel(pvnames.double_pv) + self.assertIsNot(chid, None) + + def testA_GetNonExistentPV(self): + write('Simple Test: get on a non-existent PV') + chid = ca.create_channel('Definitely-Not-A-Real-PV') + val = ca.get(chid) + self.assertEqual(val, None) + + def testA_CreateChid_CheckTypeCount(self): + write('Simple Test: create chid, check count, type, host, and access') + chid = ca.create_channel(pvnames.double_pv) + ret = ca.connect_channel(chid) + ca.pend_event(1.e-3) + + ftype = ca.field_type(chid) + count = ca.element_count(chid) + host = ca.host_name(chid) + rwacc = ca.access(chid) + + self.assertIsNot(chid, None) + self.assertIsNot(host, None) + self.assertEqual(count, 1) + self.assertEqual(ftype, 6) + self.assertEqual(rwacc,'read/write') + + + def testA_CreateChidWithConn(self): + write('Simple Test: create chid with conn callback') + chid = ca.create_channel(pvnames.int_pv, + callback=onConnect) + val = ca.get(chid) + + global CONN_DAT + conn = CONN_DAT.get(pvnames.int_pv, None) + self.assertEqual(conn, True) + + def test_dbrName(self): + write( 'DBR Type Check') + self.assertEqual(dbr.Name(dbr.STRING), 'STRING') + self.assertEqual(dbr.Name(dbr.FLOAT), 'FLOAT') + self.assertEqual(dbr.Name(dbr.ENUM), 'ENUM') + self.assertEqual(dbr.Name(dbr.CTRL_CHAR), 'CTRL_CHAR') + self.assertEqual(dbr.Name(dbr.TIME_DOUBLE), 'TIME_DOUBLE') + self.assertEqual(dbr.Name(dbr.TIME_LONG), 'TIME_LONG') + + self.assertEqual(dbr.Name('STRING', reverse=True), dbr.STRING) + self.assertEqual(dbr.Name('DOUBLE', reverse=True), dbr.DOUBLE) + self.assertEqual(dbr.Name('CTRL_ENUM', reverse=True), dbr.CTRL_ENUM) + self.assertEqual(dbr.Name('TIME_LONG', reverse=True), dbr.TIME_LONG) + + def test_Connect1(self): + chid = ca.create_channel(pvnames.double_pv) + conn,dt,n = _ca_connect(chid, timeout=2) + write( 'CA Connection Test1: connect to existing PV') + write( ' connected in %.4f sec' % (dt)) + self.assertEqual(conn,True) + + def test_Connected(self): + pvn = pvnames.double_pv + chid = ca.create_channel(pvn,connect=True) + isconn = ca.isConnected(chid) + write( 'CA test Connected (%s) = %s' % (pvn,isconn)) + self.assertEqual(isconn,True) + state= ca.state(chid) + self.assertEqual(state,ca.dbr.CS_CONN) + acc = ca.access(chid) + self.assertEqual(acc,'read/write') + + + def test_DoubleVal(self): + pvn = pvnames.double_pv + chid = ca.create_channel(pvn,connect=True) + cdict = ca.get_ctrlvars(chid) + write( 'CA testing CTRL Values for a Double (%s)' % (pvn)) + self.failUnless('units' in cdict) + self.failUnless('precision' in cdict) + self.failUnless('severity' in cdict) + + hostname = ca.host_name(chid) + self.failUnless(len(hostname) > 1) + + count = ca.element_count(chid) + self.assertEqual(count,1) + + ftype= ca.field_type(chid) + self.assertEqual(ftype,ca.dbr.DOUBLE) + + prec = ca.get_precision(chid) + self.assertEqual(prec, pvnames.double_pv_prec) + + units= ca.BYTES2STR(ca.get_ctrlvars(chid)['units']) + self.assertEqual(units, pvnames.double_pv_units) + + rwacc= ca.access(chid) + self.failUnless(rwacc.startswith('read')) + + + def test_UnConnected(self): + write( 'CA Connection Test1: connect to non-existing PV (2sec timeout)') + chid = ca.create_channel('impossible_pvname_certain_to_fail') + conn,dt,n = _ca_connect(chid, timeout=2) + self.assertEqual(conn,False) + + + def test_putwait(self): + 'test put with wait' + pvn = pvnames.non_updating_pv + chid = ca.create_channel(pvn, connect=True) + o = ca.put(chid, -1, wait=True) + val = ca.get(chid) + self.assertEqual(val, -1) + o = ca.put(chid, 2, wait=True) + val = ca.get(chid) + self.assertEqual(val, 2) + + def test_promote_type(self): + pvn = pvnames.double_pv + chid = ca.create_channel(pvn,connect=True) + write( 'CA promote type (%s)' % (pvn)) + f_t = ca.promote_type(chid,use_time=True) + f_c = ca.promote_type(chid,use_ctrl=True) + self.assertEqual(f_t, ca.dbr.TIME_DOUBLE) + self.assertEqual(f_c, ca.dbr.CTRL_DOUBLE) + + def test_ProcPut(self): + pvn = pvnames.enum_pv + chid = ca.create_channel(pvn, connect=True) + write( 'CA test put to PROC Field (%s)' % (pvn)) + for input in (1, '1', 2, '2', 0, '0', 50, 1): + ret = None + try: + ret = ca.put(chid, 1) + except: + pass + self.assertIsNot(ret, None) + + def test_subscription_double(self): + pvn = pvnames.updating_pv1 + chid = ca.create_channel(pvn,connect=True) + cb, uarg, eventID = ca.create_subscription(chid, callback=onChanges) + + start_time = time.time() + global CHANGE_DAT + while time.time()-start_time < 5.0: + time.sleep(0.01) + if CHANGE_DAT.get(pvn, None) is not None: + break + val = CHANGE_DAT.get(pvn, None) + ca.clear_subscription(eventID) + self.assertIsNot(val, None) + + def test_subscription_custom(self): + pvn = pvnames.updating_pv1 + chid = ca.create_channel(pvn, connect=True) + + global change_count + change_count = 0 + + def my_callback(pvname=None, value=None, **kws): + write( ' Custom Callback %s value=%s' %(pvname, str(value))) + global change_count + change_count = change_count + 1 + + cb, uarg, eventID = ca.create_subscription(chid, callback=my_callback) + + start_time = time.time() + while time.time()-start_time < 2.0: + time.sleep(0.01) + + ca.clear_subscription(eventID) + time.sleep(0.2) + self.assertTrue(change_count > 2) + + def test_subscription_str(self): + + pvn = pvnames.updating_str1 + write(" Subscription on string: %s " % pvn) + chid = ca.create_channel(pvn,connect=True) + cb, uarg, eventID = ca.create_subscription(chid, callback=onChanges) + + start_time = time.time() + global CHANGE_DAT + while time.time()-start_time < 3.0: + time.sleep(0.01) + ca.put(chid, "%.1f" % (time.time()-start_time) ) + if CHANGE_DAT.get(pvn, None) is not None: + break + val = CHANGE_DAT.get(pvn, None) + # ca.clear_subscription(eventID) + self.assertIsNot(val, None) + time.sleep(0.2) + + + def _test_array_callback(self, arrayname, array_type, length, element_type): + """ Helper function to subscribe to a PV array and check it + receives at least one subscription callback w/ specified type, + length & uniform element type. Checks separately for normal, + TIME & CTRL subscription variants. Returns the array or fails + an assertion.""" + results = {} + for form in [ 'normal', 'time', 'ctrl' ]: + chid = ca.create_channel(arrayname,connect=True) + cb, uarg, eventID = ca.create_subscription(chid, use_time=form=='time', use_ctrl=form=='ctrl', callback=onChanges) + + CHANGE_DAT.pop(arrayname, None) + timeout=0 + # wait up to 6 seconds, if no callback probably due to simulator.py + # not running... + while timeout<120 and not arrayname in CHANGE_DAT: + time.sleep(0.05) + timeout = timeout+1 + val = CHANGE_DAT.get(arrayname, None) + ca.clear_subscription(eventID) + self.assertIsNot(val, None) + self.assertEqual(type(val), array_type) + self.assertEqual(len(val), length) + self.assertEqual(type(val[0]), element_type) + self.assertTrue(all( type(e)==element_type for e in val)) + results[form] = val + return results + + def test_subscription_long_array(self): + """ Check that numeric arrays callbacks successfully send correct data """ + self._test_array_callback(pvnames.long_arr_pv, numpy.ndarray, 2048, numpy.int32) + + def test_subscription_double_array(self): + """ Check that double arrays callbacks successfully send correct data """ + self._test_array_callback(pvnames.double_arr_pv, numpy.ndarray, 2048, numpy.float64) + + def test_subscription_string_array(self): + """ Check that string array callbacks successfully send correct data """ + results = self._test_array_callback(pvnames.string_arr_pv, list, 128, str) + self.assertTrue(len(results["normal"][0]) > 0) + self.assertTrue(len(results["time"][0]) > 0) + self.assertTrue(len(results["ctrl"][0]) > 0) + + def test_subscription_char_array(self): + """ Check that uchar array callbacks successfully send correct data as arrays """ + self._test_array_callback(pvnames.char_arr_pv, numpy.ndarray, 128, numpy.uint8) + + + + def test_Values(self): + write( 'CA test Values (compare 5 values with caget)') + os.system('rm ./caget.tst') + vals = {} + with no_simulator_updates(): + for pvn in (pvnames.str_pv, pvnames.int_pv, + pvnames.float_pv, pvnames.enum_pv, + pvnames.long_pv): + os.system('caget -n -f5 %s >> ./caget.tst' % pvn) + chid = ca.create_channel(pvn) + ca.connect_channel(chid) + vals[pvn] = ca.get(chid) + rlines = open('./caget.tst', 'r').readlines() + for line in rlines: + pvn, sval = [i.strip() for i in line[:-1].split(' ', 1)] + tval = str(vals[pvn]) + if pvn in (pvnames.float_pv,pvnames.double_pv): + # use float precision! + tval = "%.5f" % vals[pvn] + self.assertEqual(tval, sval) + + def test_type_converions_1(self): + write("CA type conversions scalars") + pvlist = (pvnames.str_pv, pvnames.int_pv, pvnames.float_pv, + pvnames.enum_pv, pvnames.long_pv, pvnames.double_pv2) + chids = [] + with no_simulator_updates(): + for name in pvlist: + chid = ca.create_channel(name) + ca.connect_channel(chid) + chids.append((chid, name)) + ca.poll(evt=0.025, iot=5.0) + ca.poll(evt=0.05, iot=10.0) + + values = {} + for chid, name in chids: + values[name] = ca.get(chid, as_string=True) + + for promotion in ('ctrl', 'time'): + for chid, pvname in chids: + write('=== %s chid=%s as %s' % (ca.name(chid), repr(chid), + promotion)) + time.sleep(0.01) + if promotion == 'ctrl': + ntype = ca.promote_type(chid, use_ctrl=True) + else: + ntype = ca.promote_type(chid, use_time=True) + + val = ca.get(chid, ftype=ntype) + cval = ca.get(chid, as_string=True) + if ca.element_count(chid) > 1: + val = val[:12] + self.assertEqual(cval, values[pvname]) + + def test_type_converions_2(self): + write("CA type conversions arrays") + pvlist = (pvnames.char_arr_pv, + pvnames.long_arr_pv, + pvnames.double_arr_pv) + with no_simulator_updates(): + chids = [] + for name in pvlist: + chid = ca.create_channel(name) + ca.connect_channel(chid) + chids.append((chid, name)) + ca.poll(evt=0.025, iot=5.0) + ca.poll(evt=0.05, iot=10.0) + + values = {} + for chid, name in chids: + values[name] = ca.get(chid) + for promotion in ('ctrl', 'time'): + for chid, pvname in chids: + write('=== %s chid=%s as %s' % (ca.name(chid), repr(chid), + promotion)) + time.sleep(0.01) + if promotion == 'ctrl': + ntype = ca.promote_type(chid, use_ctrl=True) + else: + ntype = ca.promote_type(chid, use_time=True) + + val = ca.get(chid, ftype=ntype) + cval = ca.get(chid, as_string=True) + for a, b in zip(val, values[pvname]): + self.assertEqual(a, b) + + + def test_Array0(self): + write('Array Test: get double array as numpy array, ctypes Array, and list') + chid = ca.create_channel(pvnames.double_arrays[0]) + aval = ca.get(chid) + cval = ca.get(chid, as_numpy=False) + + self.assertTrue(isinstance(aval, numpy.ndarray)) + self.assertTrue(len(aval) > 2) + + self.assertTrue(isinstance(cval, ctypes.Array)) + self.assertTrue(len(cval) > 2) + lval = list(cval) + self.assertTrue(isinstance(lval, list)) + self.assertTrue(len(lval) > 2) + self.assertTrue(lval == list(aval)) + + def test_xArray1(self): + write('Array Test: get(wait=False) / get_complete()') + chid = ca.create_channel(pvnames.double_arrays[0]) + val0 = ca.get(chid) + aval = ca.get(chid, wait=False) + self.assertTrue(aval is None) + val1 = ca.get_complete(chid) + self.assertTrue(all(val0 == val1)) + + def test_xArray2(self): + write('Array Test: get fewer than max vals using ca.get(count=0)') + chid = ca.create_channel(pvnames.double_arrays[0]) + maxpts = ca.element_count(chid) + npts = int(max(2, maxpts/2.3 - 1)) + write('max points is %s' % (maxpts, )) + dat = numpy.random.normal(size=npts) + write('setting array to a length of npts=%s' % (npts, )) + ca.put(chid, dat) + out1 = ca.get(chid) + self.assertTrue(isinstance(out1, numpy.ndarray)) + self.assertEqual(len(out1), npts) + out2 = ca.get(chid, count=0) + self.assertTrue(isinstance(out2, numpy.ndarray)) + self.assertEqual(len(out2), npts) + + def test_xArray3(self): + write('Array Test: get char array as string') + chid = ca.create_channel(pvnames.char_arrays[0]) + val = ca.get(chid) + self.assertTrue(isinstance(val, numpy.ndarray)) + char_val = ca.get(chid, as_string=True) + self.assertTrue(isinstance(char_val, str)) + conv = ''.join([chr(i) for i in val]) + self.assertEqual(conv, char_val) + + +if __name__ == '__main__': + suite = unittest.TestLoader().loadTestsFromTestCase( CA_BasicTests) + unittest.TextTestRunner(verbosity=1).run(suite) + + +# chid = ca.create_channel(pvnames.int_pv, +# callback=onConnect) +# +# time.sleep(0.1) +# ; diff --git a/tests/caget_large_arrays_slow_net.py b/tests/caget_large_arrays_slow_net.py new file mode 100644 index 0000000..4d8ef83 --- /dev/null +++ b/tests/caget_large_arrays_slow_net.py @@ -0,0 +1,35 @@ +# tests caget (and so pv.get() and ca.get()) for "large arrays" +# especially for "slow networks". +# the 3 pvs listed here are: +# 13GEXMAP:mca1: 2048 ints (simple MCA record) +# GSE-PIL1:image1:ArrayData 94965 longs (Pilatus 100k) +# 13MARCCD1:image1:ArrayData 420000 ints (MAR165) +# obviously, you'll need to alter these names +# +# TIMEOUT sets the pend_io() time. If working with large +# arrays and/or slow networks, you may want to test for a +# suitable safe timeout. +import epics +import time +pvnames = ('13GEXMAP:mca1', + 'GSE-PIL1:image1:ArrayData', + '13MARCCD2:image1:ArrayData', + ) + +TIMEOUT = 15.0 # Timeout in seconds + +for name in pvnames: + t0 = time.time() + value = epics.caget(name, timeout=TIMEOUT) + dt = time.time()-t0 + if value is None: + print( 'cannot get value for ', name) + else: + print( "%s: npts=%i, sum=%i, max=%i, time=%.3fs" % (name, + len(value), + value.sum(), + value.max(), dt)) + +print( 'done.') + + diff --git a/tests/cathread_tests.py b/tests/cathread_tests.py new file mode 100644 index 0000000..e45b35e --- /dev/null +++ b/tests/cathread_tests.py @@ -0,0 +1,110 @@ +"""This script tests using EPICS CA and Python threads together + + Based on code from Friedrich Schotte, NIH + modified by Matt Newville 19-Apr-2010 + + modified MN, 22-April-2011 (1 year later!) + to support new context-switching modes + +""" + +import time +import epics +import sys +from threading import Thread +from epics.ca import CAThread, withInitialContext +from pvnames import updating_pvlist +write = sys.stdout.write +flush = sys.stdout.flush + +epics.ca.PREEMPTIVE_CALLBACK=True + +def wait_for_changes(pvnames, runtime, runname): + """basic test procedure called by other tests + """ + def onChanges(pvname=None, value=None, char_value=None, **kw): + write(' %s= %s (%s)\n' % (pvname, char_value, runname)) + flush() + t0 = time.time() + pvs = [] + for pvn in pvnames: + p = epics.PV(pvn) + p.get() + p.add_callback(onChanges) + pvs.append(p) + + while time.time()-t0 < runtime: + try: + time.sleep(0.01) + except: + sys.exit() + + for p in pvs: + p.clear_callbacks() + +def test_initcontext(pvnames, runtime, run_name): + write(' -> force inital ca context: thread=%s will run for %.3f sec\n' % (run_name, runtime)) + + + epics.ca.use_initial_context() + wait_for_changes(pvnames, runtime, run_name) + + write( 'Done with Thread %s\n' % ( run_name)) + +@withInitialContext +def test_decorator(pvnames, runtime, run_name): + write(' -> use withInitialContext decorator: thread=%s will run for %.3f sec\n' % (run_name, runtime)) + + wait_for_changes(pvnames, runtime, run_name) + + write( 'Done with Thread %s\n' % ( run_name)) + +def test_CAThread(pvnames, runtime, run_name): + write(' -> used with CAThread: thread=%s will run for %.3f sec\n' % (run_name, runtime)) + + wait_for_changes(pvnames, runtime, run_name) + write( 'Done with Thread %s\n' % ( run_name)) + +def run_threads(threadlist): + for th in threadlist: + th.start() + time.sleep(0.01) + for th in threadlist: + th.join() + time.sleep(0.01) + +# MAIN +write("Connecting to PVs\n") +pvs_b = [] +names_b = [] +for pvname in updating_pvlist: + ###pvs_b.append(epics.PV(pvname)) + # pvs_b.append(pvname) + names_b.append(pvname) + +names_a = names_b[1:] +pvs_a = pvs_b[1:] + +epics.ca.create_context() + +styles = ('decorator', 'init', 'cathread') +style = styles[2] + +if style == 'init': + write( 'Test use plain threading.Thread, force use of initial CA Context \n') + th1 = Thread(target=test_initcontext, args=(names_a, 2, 'A')) + th2 = Thread(target=test_initcontext, args=(names_b, 3, 'B')) + run_threads((th1, th2)) + +elif style == 'decorator': + write( 'Test use plain threading.Thread, withInitialContext decorator\n') + th1 = Thread(target=test_decorator, args=(names_a, 3, 'A')) + th2 = Thread(target=test_decorator, args=(names_b, 5, 'B')) + run_threads((th1, th2)) +elif style == 'cathread': + write( 'Test use CAThread\n') + th1 = CAThread(target=test_CAThread, args=(names_a, 3, 'A')) + th2 = CAThread(target=test_CAThread, args=(names_b, 5, 'B')) + run_threads((th1, th2)) + +write('Test Done\n---------------------\n') diff --git a/tests/connect.py b/tests/connect.py new file mode 100644 index 0000000..5e4748d --- /dev/null +++ b/tests/connect.py @@ -0,0 +1,68 @@ +from __future__ import print_function + +import sys +import time + +import epics + +from pvnames import motor_list + +pvnames= [] +for a in ('VAL','RBV','DVAL', 'RVAL','LLM','HLM','DIR','OFF', + 'FOFF','VELO','VBAS','ACCL','DESC','MRES'): + for m in motor_list: + pvnames.append("%s.%s" %(m,a)) + +print( pvnames) + +def testconnect(pvnames,connect=True): + t0 = time.time() + pvlist= [] + for pvname in pvnames: + x = epics.PV(pvname) + if connect: + x.connect() + pvlist.append(x) + + for x in pvlist: + x.get() + + dt = time.time()-t0 +# for x in pvlist: +# x.get() + sys.stdout.write('===Connect with PV(connect=%s) to %i pvs\n' % (connect, len(pvlist))) + sys.stdout.write(' Total Time = %.4f s, Time per PV = %.1f ms\n' % ( dt, 1000.*dt/len(pvlist))) + + +sys.stdout.write( """ +Test connection time for many PVs. + +With: + pvs = [] + for pvn in pvnames: + x = epics.PV(pvn) + x.connect() + pvs.append(x) + + for x in pvs: x.get() + +connection takes 30ms per PV + +With + pvs = [] + for pvname in pvlist: + x = PV(pvname) + pvs.append(x) + + for x in pvs: x.get() + +connection takes less than 8ms per PV. +""") + +testconnect(pvnames, False) + +epics.ca._cache = {} + +testconnect(pvnames, True) + +print( 'Done') diff --git a/tests/debugtime.py b/tests/debugtime.py new file mode 100755 index 0000000..764fb19 --- /dev/null +++ b/tests/debugtime.py @@ -0,0 +1,44 @@ +import time +import sys +class debugtime(object): + def __init__(self): + self.clear() + self.add('debugtime init') + + def clear(self): + self.times = [] + + def add(self,msg='', verbose=False): + # print msg + self.times.append((msg,time.time())) + if verbose: + sys.stdout.write("%s\n"% msg) + + def show(self, writer=None, clear=True): + if writer is None: + writer = sys.stdout.write + writer('%s\n' % self.get_report()) + if clear: + self.clear() + + + def get_report(self): + m0, t0 = self.times[0] + tlast= t0 + out = ["# %s %s " % (m0,time.ctime(t0))] + lmsg = 0 + for m, t in self.times[1:]: + lmsg = max(lmsg, len(m)) + m = '# Message' + m = m + ' '*(lmsg-len(m)) + + out.append("#--------------------" + '-'*lmsg) + out.append('%s Delta(s) Total(s)' % (m)) + for m,t in self.times[1:]: + tt = t-t0 + dt = t-tlast + if len(m)= nnotify: + print( "Saw %i changes in %.3f seconds: %s" % (N_new, time.time()-t0, show_memory())) + N_new = 0 + t0 = time.time() + +def run(t=10.0): + pvs = [epics.PV(i, callback=get_callback) for i in pvlist] + epics.ca.pend_io(1.0) + for p in pvs: p.get() + print( 'Monitoring %i PVs' % len(pvs)) + monitor_events(t=t) + + print( 'Destroying PVs: ') + for i in pvs: + i.disconnect() + print( epics.ca._cache.keys()) + epics.ca.show_cache() + epics.ca.poll(0.01, 10.0) + time.sleep(1.0) + +for i in range(4): + print( "==run # ", i+1) + run(t=15) + +print( 'memory leak test complete.') + + diff --git a/tests/memleak_put.py b/tests/memleak_put.py new file mode 100644 index 0000000..8b0ca91 --- /dev/null +++ b/tests/memleak_put.py @@ -0,0 +1,57 @@ +from __future__ import print_function + +import time +import gc +import os +import epics + +# a test for possible memory leaks on put() +import pvnames + +pvlist = pvnames.char_arrays # + pvnames.long_arrays + pvnames.double_arrays + +def show_memory(): + gc.collect() + if os.name == 'nt': + return 'Windows memory usage?? pid=%i' % os.getpid() + f = open("/proc/%i/statm" % os.getpid()) + mem = f.readline().split() + f.close() + return 'Memory: VmSize = %i kB / VmRss = %i kB' %( int(mem[0])*4 , int(mem[1])*4) + +N_new = 0 +def get_callback(pv=None, **kws): + global N_new + N_new = N_new + 1 + # print( 'New value: ', pv.pvname, pv.char_value) + +def monitor_events(t = 600.0): + print('Processing PV requests:') + t0 = time.time() + endtime = t0 + t + nx = 0 + global N_new + nnotify = int(t / 30) + while time.time() < endtime: + epics.ca.pend_event(0.05) + nx = nx + 1 + if nx >=nnotify: + print("changes (%i) / %.3f / %s" % (N_new, time.time()-t0, show_memory())) + N_new = 0 + nx = 0 + +pvs = [epics.PV(i, callback=get_callback) for i in pvlist] +epics.ca.pend_io() + +for i in range(500): + for p in pvs: + p.put('test: run %i' % (i)) + epics.ca.pend_event(0.02) + if i%20 == 0: + print("==run # ", i, show_memory()) + time.sleep(0.02) + +epics.ca.pend_io(1.0) + +print('really done.') + diff --git a/tests/memory_motor.py b/tests/memory_motor.py new file mode 100644 index 0000000..334ca7d --- /dev/null +++ b/tests/memory_motor.py @@ -0,0 +1,51 @@ +import time +import sys + +import epics +import gc + +import pvnames + +import os +def show_memory(): + gc.collect() + if os.name == 'nt': + return 'Windows memory usage?? pid=%i' % os.getpid() + + f = open("/proc/%i/statm" % os.getpid()) + mem = f.readline().split() + f.close() + sys.stdout.write('Memory: VmSize = %i kB / VmRss = %i kB\n' %( int(mem[0])*4 , int(mem[1])*4)) + +def get_callback(pvname=None, char_value=None,**kw): + sys.stdout.write('OnGet %s: %s\n' % (pvname, char_value)) + +def monitor_events(t = 10.0): + sys.stdout.write('Processing PV requests:\n') + t0 = time.time() + while time.time()-t0 < t : + epics.ca.poll() + +def round(): + sys.stdout.write('== Creating some PVs\n ') + pvs = [] + for field in ('VAL','DESC', 'OFF','FOFF', 'HLM','LLM','SET'): + pvs.append(epics.PV("%s.%s" % (pvnames.motor1,field), callback=get_callback) ) + epics.ca.poll() + + for p in pvs: p.connect() + monitor_events(t=4.) + + epics.ca.show_cache() + sys.stdout.write('Destroying PVs:\n ') + sys.stdout.flush() + for i in pvs: i.disconnect() + + monitor_events(t=0.5) + +for i in range(20): + round() + show_memory() + +epics.ca.pend_io(1.0) + diff --git a/tests/motor_simple.py b/tests/motor_simple.py new file mode 100644 index 0000000..ca68787 --- /dev/null +++ b/tests/motor_simple.py @@ -0,0 +1,34 @@ +from __future__ import print_function +import epics +import time +import pvnames + +epics.ca.DEFAULT_CONNECTION_TIMEOUT=10. + +def test1(motorname, start, step, npts): + "simple test: stepping with wait" + m1 = epics.Motor(motorname) + m1.drive = start + m1.tweak_val = step + m1.move(start, wait=True) + + for i in range(npts): + m1.tweak(dir='forward', wait=True) + print('Motor: ', m1.description , m1.drive, ' Currently at ', m1.readback) + time.sleep(0.01) + +def testDial(motorname,start, step, npts, offset=1.0): + "test using dial coordinates" + m1 = epics.Motor(motorname) + m1.offset = offset + m1.tweak_val = step + m1.move(start, wait=True, dial=True) + + print('Motor position ', motorname, m1.description) + user = m1.get_position() + dial = m1.get_position(dial=True) + raw = m1.get_position(raw=True) + print(' User/Dial/Raw = %f / %f / %f' % (user, dial, raw)) + + +testDial(pvnames.motor1, 0.5, 0.01, 10, offset=0.1) diff --git a/tests/no_monitor.py b/tests/no_monitor.py new file mode 100644 index 0000000..b5648bd --- /dev/null +++ b/tests/no_monitor.py @@ -0,0 +1,17 @@ +from __future__ import print_function +import time +import epics +import pvnames + +p = epics.PV(pvnames.updating_pv1, auto_monitor= False) + +def onChange(pvname=None,char_value=None,value=None,**kw): + print(pvname, value, time.ctime()) +p.add_callback(onChange) + +t0 = time.time() +p.get() +while time.time()-t0 < 20: + print(p.get()) + time.sleep(0.1) + diff --git a/tests/o.py b/tests/o.py new file mode 100644 index 0000000..6c86282 --- /dev/null +++ b/tests/o.py @@ -0,0 +1,44 @@ +from ctypes import cdll +from epics.ca import find_libca +dllname = find_libca() + +print(dllname) + +libca = cdll.LoadLibrary(dllname) +print("{:20s} {:s} {:s}".format('function name', 'in libca' , 'in None')) + +for fname in ('ca_context_create', 'ca_pend_io', + 'ca_create_channel'): + + print("{:20s} {:s} {:s}".format(fname, + repr(hasattr(libca, fname)), + repr(hasattr(cdll.LoadLibrary(None), fname)))) + +# +# False +# >>> libca.ca_context_create +# <_FuncPtr object at 0x109536c00> +# >>> hasattr(cdll.LoadLibrary(None), 'sqrt') +# True +# >>> libc = cdll.LoadLibrary('libc') +# Traceback (most recent call last): +# File "", line 1, in +# File "/Users/Newville/anaconda3/lib/python3.7/ctypes/__init__.py", line 434, in LoadLibrary +# return self._dlltype(name) +# File "/Users/Newville/anaconda3/lib/python3.7/ctypes/__init__.py", line 356, in __init__ +# self._handle = _dlopen(self._name, mode) +# OSError: dlopen(libc, 6): image not found +# >>> libc = cdll.LoadLibrary('libc.dylib') +# >>> hasattr(cdll.LoadLibrary(None), 'sqrt') +# True +# >>> libca.ca_context_create +# <_FuncPtr object at 0x109536c00> +# >>> hasattr(cdll.LoadLibrary(None), 'ca_context_create') +# False +# >>> hasattr(cdll.LoadLibrary(None), 'ca_pend_io') +# False +# >>> hasattr(cdll.LoadLibrary(None), 'ca_pend_event') +# False +# >>> libca.ca_pend_event +# <_FuncPtr object at 0x109536e58> +# >>> diff --git a/tests/o1.py b/tests/o1.py new file mode 100644 index 0000000..b5a3753 --- /dev/null +++ b/tests/o1.py @@ -0,0 +1,10 @@ +from ctypes import cdll +from epics.ca import find_libca +dllname = find_libca() + +print(dllname) + +libca = cdll.LoadLibrary(dllname) + +print(hasattr(libca, 'ca_context_create'), + hasattr(cdll.LoadLibrary(None), 'ca_context_create')) diff --git a/tests/putwait.py b/tests/putwait.py new file mode 100644 index 0000000..bc6dbdf --- /dev/null +++ b/tests/putwait.py @@ -0,0 +1,37 @@ +"""This script tests using EPICS CA and Python threads together + +Based on code from Friedrich Schotte, NIH, modified by Matt Newville +20-Apr-2010 +""" + +import time +import epics +import sys +import pvnames + +write = sys.stdout.write +flush = sys.stdout.flush + +write('initial put: \n') +epics.caput(pvnames.motor1, -2.0) +epics.caput(pvnames.motor2, 33.0) + +write('sleep...') +time.sleep(2.0) +flush() + +write('now put with wait: \n') +flush() + +epics.caput(pvnames.motor2, -20.0, wait=True) +write('done with move 1\n') +flush() + +epics.caput(pvnames.motor2, 20.0, wait=True) +write('done with move 2\n') + +epics.caput(pvnames.motor2, -20.0, wait=True) +write('done with move 3\n') + +epics.caput(pvnames.motor2, 20.0, wait=True) +write('done with move 4\n') diff --git a/tests/pv_callback.py b/tests/pv_callback.py new file mode 100644 index 0000000..4d5b757 --- /dev/null +++ b/tests/pv_callback.py @@ -0,0 +1,26 @@ +import time +import epics +import sys +import pvnames + +pvname = pvnames.updating_pv1 # motor1 + +mypv = epics.PV(pvname) + +write = sys.stdout.write + +mypv.get_ctrlvars() + +write('Created PV = %s\n' % mypv) +def onChanges(pvname=None, value=None, char_value=None, **kw): + write( 'PV %s %s, %s Changed!\n' % (pvname, repr(value), char_value)) + +mypv.add_callback(onChanges) + +write('Added a callback. Now wait for changes...\n') + +def wait(timeout=10): + t0 = time.time() + while time.time() - t0 < timeout: time.sleep(1.e-4) + +wait(10) diff --git a/tests/pv_connection_callback.py b/tests/pv_connection_callback.py new file mode 100644 index 0000000..3a0ee0f --- /dev/null +++ b/tests/pv_connection_callback.py @@ -0,0 +1,27 @@ +# +# example of using a connection callback that will be called +# for any change in connection status + +import epics +import time +import sys +from pvnames import motor1 + +write = sys.stdout.write +def onConnectionChange(pvname=None, conn= None, **kws): + write('PV connection status changed: %s %s\n' % (pvname, repr(conn))) + sys.stdout.flush() + +def onValueChange(pvname=None, value=None, host=None, **kws): + write('PV value changed: %s (%s) %s\n' % ( pvname, host, repr(value))) + sys.stdout.flush() +mypv = epics.PV(motor1, + connection_callback= onConnectionChange, + callback= onValueChange) + +mypv.get() + +write('Now waiting, watching values and connection changes:\n') +t0 = time.time() +while time.time()-t0 < 300: + time.sleep(0.01) diff --git a/tests/pv_disconnect_with_getcb.py b/tests/pv_disconnect_with_getcb.py new file mode 100644 index 0000000..acda00b --- /dev/null +++ b/tests/pv_disconnect_with_getcb.py @@ -0,0 +1,51 @@ +# +# example of using a connection callback that will be called +# for any change in connection status + +import epics +import time +import sys +from pvnames import updating_pv1, updating_pvlist +epics.ca.PREEMPTIVE_CALLBACK = True +write = sys.stdout.write +def onConnectionChange(pvname=None, conn= None, **kws): + write('Connection changed: %s conn=%s (%s)\n' % (pvname, repr(conn), time.ctime())) + sys.stdout.flush() + +def onValueChange(pvname=None, value=None, host=None, **kws): + write('Value changed: %s = %s (%s)\n' % ( pvname, repr(value), time.ctime())) + sys.stdout.flush() + + +# pv1 = epics.PV(updating_pv1, +# connection_callback= onConnectionChange, +# callback= onValueChange) +# +pxs = [epics.PV(thispv, + connection_callback= onConnectionChange, + callback= onValueChange) for thispv in updating_pvlist] + +for x in pxs: + write("%s = %s\n" % (x.pvname, x.get(as_string=True))) + +write('Now waiting, watching values and connection changes:\n') +t0 = time.time() +while time.time()-t0 < 300: + try: + time.sleep(0.01) + except KeyboardInterrupt: + break +# +# write('Some value changes should have been seens\n') +# write('Now, restart the IOC:\n') +# t0 = time.time() +# while time.time()-t0 < 60: +# time.sleep(0.01) +# +# write('You should have seen a connection message and new values\n') + + +write("done!\n") + +epics.ca.show_cache() + diff --git a/tests/pv_initial_callbacks.py b/tests/pv_initial_callbacks.py new file mode 100644 index 0000000..749918b --- /dev/null +++ b/tests/pv_initial_callbacks.py @@ -0,0 +1,49 @@ +import time +import epics +import sys +import pvnames + +# Test than when a PV connections all callbacks fire successfully +# +# does not require Setup/simulator.py to be running (PV is deliberately one which does not change) + +write = sys.stdout.write + +got_callback_a = False +got_callback_b = False + +def callback_a(pvname=None, value=None, **kw): + global got_callback_a + write( "Got callback A (%s, %s)\n" % (pvname, repr(value)) ) + got_callback_a = True + +def callback_b(pvname=None, value=None, **kw): + global got_callback_b + write( "Got callback B (%s, %s)\n" % (pvname, repr(value)) ) + got_callback_b = True + + +pvname = pvnames.non_updating_pv +mypv = epics.PV(pvname, callback=(callback_a, callback_b)) + +write('Created PV with two callbacks = %s\n' % mypv) + +write('Now wait for changes...\n') + +def wait(timeout=10): + t0 = time.time() + while time.time() - t0 < timeout and not (got_callback_a and got_callback_b): + time.sleep(1.e-4) + +wait(2) + +if not mypv.connected: + write('ERROR: PV never connected\n') + sys.exit(1) + +if not (got_callback_a and got_callback_b): + write('ERROR: Inconsistent initial value callbacks - callback A = %s, callback B = %s\n' + % (got_callback_a, got_callback_b) ) + sys.exit(1) + +write('Got both callbacks OK!\n') diff --git a/tests/pv_multiple_callbacks.py b/tests/pv_multiple_callbacks.py new file mode 100644 index 0000000..4b9302f --- /dev/null +++ b/tests/pv_multiple_callbacks.py @@ -0,0 +1,37 @@ +import time +import epics +import pvnames +pvname = pvnames.double_pv + +mypv = epics.PV(pvname) + + +def wait(timeout=6): + t0 = time.time() + while time.time() - t0 < timeout: + time.sleep(1.e-3) + epics.poll() + + +print( 'Created PV = ', mypv) +def CB1(pvname=None, value=None, char_value=None, **kw): + print( 'CB1 PV Changed! ', pvname, value, char_value ) + + +def CB2(pvname=None, value=None, char_value=None, **kw): + print( 'CB2 ! ', pvname, value, char_value) + +mypv.add_callback(CB1) + +print( 'Added a callback. Now wait for changes') + +print( 'ready') + +wait(10) +print( 'now, add another: ') +mypv.add_callback(CB2) + +wait(10) + + + diff --git a/tests/pv_subarray_test.py b/tests/pv_subarray_test.py new file mode 100644 index 0000000..2ccf573 --- /dev/null +++ b/tests/pv_subarray_test.py @@ -0,0 +1,43 @@ +import time +import epics +import pvnames +import random +import numpy +import sys +driver = epics.PV(pvnames.subarr_driver) +sub1 = epics.PV(pvnames.subarr1) +sub2 = epics.PV(pvnames.subarr2) +sub3 = epics.PV(pvnames.subarr3) +sub4 = epics.PV(pvnames.subarr4) + + +s1_0 = int(epics.caget("%s.INDX" % pvnames.subarr1)) +s2_0 = int(epics.caget("%s.INDX" % pvnames.subarr2)) +s3_0 = int(epics.caget("%s.INDX" % pvnames.subarr3)) +s4_0 = int(epics.caget("%s.INDX" % pvnames.subarr4)) + +s1_n = int(epics.caget("%s.NELM" % pvnames.subarr1)) +s2_n = int(epics.caget("%s.NELM" % pvnames.subarr2)) +s3_n = int(epics.caget("%s.NELM" % pvnames.subarr3)) +s4_n = int(epics.caget("%s.NELM" % pvnames.subarr4)) + +npts = len(driver.get()) + +for i in range(10): + driver.put([100*random.random() for x in range(npts)]) + time.sleep(0.1) + + full = driver.get() + sys.stdout.write("%s\n" % full) + all_ok = all( [all( sub1.get() == full[s1_0:s1_n+s1_0]), + all( sub2.get() == full[s2_0:s2_n+s2_0]), + all( sub3.get() == full[s3_0:s3_n+s3_0]), + all( sub4.get() == full[s4_0:s4_n+s4_0]) + ]) + + if not all_ok: + sys.stdout.write("FAIL: Subarrays don't match larger array!\n") + sys.exit() + +sys.stdout.write('PASSED.\n') + diff --git a/tests/pv_type_conversion.py b/tests/pv_type_conversion.py new file mode 100644 index 0000000..541525a --- /dev/null +++ b/tests/pv_type_conversion.py @@ -0,0 +1,73 @@ +import sys +import time +import epics +import pvnames + +HAS_NUMPY = False +try: + import numpy + HAS_NUMPY = True +except ImportError: + pass + +pvlist = ( + pvnames.str_pv, + pvnames.int_pv, + pvnames.float_pv, + pvnames.enum_pv, + pvnames.char_arr_pv, + pvnames.long_pv, + pvnames.long_arr_pv, + pvnames.double_pv, + pvnames.double_arr_pv, + pvnames.string_arr_pv, + ) + +def onConnect(pvname=None, **kw): + print(' on Connect %s -- %s' % (pvname, repr(kw))) + +def onChanges(pvname=None, value=None, **kw): + print(' on Change %s = %s' % (pvname, repr(value))) + + +def RunTest(pvlist, use_preempt=True, maxlen=16384, + use_numpy=True, form='native'): + msg= ">>>Run Test: %i pvs, numpy=%s, form=%s, preempt=%s" + print( msg % (len(pvlist), use_numpy, form, use_preempt)) + + epics.ca.HAS_NUMPY = use_numpy and HAS_NUMPY + epics.ca.PREEMPTIVE_CALLBACK = use_preempt + epics.ca.AUTOMONITOR_MAXLENGTH = maxlen + mypvs= [] + for pvname in pvlist: + pv = epics.PV(pvname, form=form, + # connection_callback=onConnect, + # callback=onChanges + ) + mypvs.append(pv) + epics.poll(evt=0.10, iot=10.0) + + for pv in mypvs: + # time.sleep(0.1) + # epics.poll(evt=0.01, iot=1.0) + val = pv.get() + cval = pv.get(as_string=True) + if pv.count > 1: + val = val[:12] + print( '-> ', pv, cval) + print( ' ', type(val), val) + for pv in mypvs: + pv.disconnect() + time.sleep(0.01) + + +for use_preempt in (True, False): + for use_numpy in (False,): + for form in ('native', 'time', 'ctrl'): + time.sleep(0.001) + RunTest(pvlist, + use_preempt=use_preempt, + use_numpy=use_numpy, + form=form) + # sys.exit() + diff --git a/tests/pv_unittest.py b/tests/pv_unittest.py new file mode 100755 index 0000000..0c64ef0 --- /dev/null +++ b/tests/pv_unittest.py @@ -0,0 +1,626 @@ +#!/usr/bin/env python +# unit-tests for ca interface + +import sys +import time +import unittest +import numpy +import threading +import pytest + +from contextlib import contextmanager +from epics import PV, get_pv, caput, caget, caget_many, caput_many, ca + +import pvnames + +def write(msg): + sys.stdout.write(msg) + sys.stdout.flush() + +CONN_DAT ={} +CHANGE_DAT = {} + +def onConnect(pvname=None, conn=None, chid=None, **kws): + write(' :Connection status changed: %s connected=%s\n' % (pvname, conn)) + global CONN_DAT + CONN_DAT[pvname] = conn + +def onChanges(pvname=None, value=None, **kws): + write( '/// New Value: %s value=%s, kw=%s\n' %( pvname, str(value), repr(kws))) + global CHANGE_DAT + CHANGE_DAT[pvname] = value + +@contextmanager +def no_simulator_updates(): + '''Context manager which pauses and resumes simulator PV updating''' + try: + caput(pvnames.pause_pv, 1, wait=True) + time.sleep(0.05) + yield + finally: + caput(pvnames.pause_pv, 0, wait=True) + + +class PV_Tests(unittest.TestCase): + def testA_CreatePV(self): + write('Simple Test: create pv\n') + pv = get_pv(pvnames.double_pv) + self.assertIsNot(pv, None) + + def testA_CreatedWithConn(self): + write('Simple Test: create pv with conn callback\n') + pv = get_pv(pvnames.int_pv, connection_callback=onConnect) + val = pv.get() + + global CONN_DAT + conn = CONN_DAT.get(pvnames.int_pv, None) + self.assertEqual(conn, True) + + def test_caget(self): + write('Simple Test of caget() function\n') + pvs = (pvnames.double_pv, pvnames.enum_pv, pvnames.str_pv) + for p in pvs: + val = caget(p) + self.assertIsNot(val, None) + 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 = 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 = get_pv(pvnames.string_arr_pv) + val = pv.get() + self.failUnless(len(val) > 10) + self.assertIsInstance(val[0], str) + self.failUnless(len(val[0]) > 1) + self.assertIsInstance(val[1], str) + self.failUnless(len(val[1]) > 1) + + 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 = 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') + + see_complete = [] + for v in vals: + t0 = time.time() + p.put(v, use_complete=True) + count = 0 + for i in range(100000): + time.sleep(0.001) + count = count + 1 + if p.put_complete: + see_complete.append(True) + break + # print( 'made it to value= %.3f, elapsed time= %.4f sec (count=%i)' % (v, time.time()-t0, count)) + self.failUnless(len(see_complete) > (len(vals) - 5)) + + def test_putwait(self): + write('Put with wait (using real motor!) \n') + 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') + + val = pv.get() + + t0 = time.time() + if val < 5: + pv.put(val + 1.0, wait=True) + else: + pv.put(val - 1.0, wait=True) + dt = time.time()-t0 + write(' put took %s sec\n' % dt) + self.failUnless( dt > 0.1) + + # now with a callback! + global put_callback_called + put_callback_called = False + + def onPutdone(pvname=None, **kws): + print( 'put done ', pvname, kws) + global put_callback_called + put_callback_called = True + val = pv.get() + if val < 5: + pv.put(val + 1.0, callback=onPutdone) + else: + pv.put(val - 1.0, callback=onPutdone) + + t0 = time.time() + while time.time()-t0 < dt*1.50: + time.sleep(0.02) + + write(' put should be done by now? %s \n' % put_callback_called) + self.failUnless(put_callback_called) + + # now using pv.put_complete + val = pv.get() + if val < 5: + pv.put(val + 1.0, use_complete=True) + else: + pv.put(val - 1.0, use_complete=True) + t0 = time.time() + count = 0 + while time.time()-t0 < dt*1.50: + if pv.put_complete: + break + count = count + 1 + time.sleep(0.02) + write(' put_complete=%s (should be True), and count=%i (should be>3)\n' % + (pv.put_complete, count)) + self.failUnless(pv.put_complete) + self.failUnless(count > 3) + + def test_get_callback(self): + write("Callback test: changing PV must be updated\n") + global NEWVALS + 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)) + NEWVALS.append( repr(value)) + + mypv.add_callback(onChanges) + write('Added a callback. Now wait for changes...\n') + + t0 = time.time() + while time.time() - t0 < 3: + time.sleep(1.e-4) + write(' saw %i changes.\n' % len(NEWVALS)) + self.failUnless(len(NEWVALS) > 3) + mypv.clear_callbacks() + + def test_put_string_waveform(self): + write('String Array: put\n') + with no_simulator_updates(): + 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) + numpy.testing.assert_array_equal(get_value, put_value) + + def test_put_string_waveform_single_element(self): + write('String Array: put single element\n') + with no_simulator_updates(): + pv = get_pv(pvnames.string_arr_pv) + put_value = ['a'] + pv.put(put_value, wait=True) + time.sleep(0.05) + get_value = pv.get(use_monitor=False, as_numpy=False) + self.failUnless(put_value[0] == get_value) + + def test_put_string_waveform_mixed_types(self): + write('String Array: put mixed types\n') + with no_simulator_updates(): + pv = get_pv(pvnames.string_arr_pv) + put_value = ['a', 2, 'b'] + pv.put(put_value, wait=True) + time.sleep(0.05) + get_value = pv.get(use_monitor=False, as_numpy=False) + numpy.testing.assert_array_equal(get_value, ['a', '2', 'b']) + + def test_put_string_waveform_empty_list(self): + write('String Array: put empty list\n') + with no_simulator_updates(): + pv = get_pv(pvnames.string_arr_pv) + put_value = [] + pv.put(put_value, wait=True) + time.sleep(0.05) + get_value = pv.get(use_monitor=False, as_numpy=False) + self.failUnless('' == ''.join(get_value)) + + def test_put_string_waveform_zero_length_strings(self): + write('String Array: put zero length strings\n') + with no_simulator_updates(): + pv = get_pv(pvnames.string_arr_pv) + put_value = ['', '', ''] + pv.put(put_value, wait=True) + time.sleep(0.05) + get_value = pv.get(use_monitor=False, as_numpy=False) + numpy.testing.assert_array_equal(get_value, put_value) + + def test_subarrays(self): + write("Subarray test: dynamic length arrays\n") + driver = get_pv(pvnames.subarr_driver) + subarr1 = get_pv(pvnames.subarr1) + subarr1.connect() + + len_full = 64 + len_sub1 = 16 + full_data = numpy.arange(len_full)/1.0 + + caput("%s.NELM" % pvnames.subarr1, len_sub1) + caput("%s.INDX" % pvnames.subarr1, 0) + + + driver.put(full_data) ; + time.sleep(0.1) + subval = subarr1.get() + + self.assertEqual(len(subval), len_sub1) + self.failUnless(numpy.all(subval == full_data[:len_sub1])) + write("Subarray test: C\n") + caput("%s.NELM" % pvnames.subarr2, 19) + caput("%s.INDX" % pvnames.subarr2, 3) + + subarr2 = get_pv(pvnames.subarr2) + subarr2.get() + + driver.put(full_data) ; time.sleep(0.1) + subval = subarr2.get() + + self.assertEqual(len(subval), 19) + self.failUnless(numpy.all(subval == full_data[3:3+19])) + + caput("%s.NELM" % pvnames.subarr2, 5) + caput("%s.INDX" % pvnames.subarr2, 13) + + driver.put(full_data) ; time.sleep(0.1) + subval = subarr2.get() + + self.assertEqual(len(subval), 5) + self.failUnless(numpy.all(subval == full_data[13:5+13])) + + def test_subarray_zerolen(self): + subarr1 = get_pv(pvnames.zero_len_subarr1) + subarr1.wait_for_connection() + + val = subarr1.get(use_monitor=True, as_numpy=True) + self.assertIsInstance(val, numpy.ndarray, msg='using monitor') + self.assertEquals(len(val), 0, msg='using monitor') + self.assertEquals(val.dtype, numpy.float64, msg='using monitor') + + val = subarr1.get(use_monitor=False, as_numpy=True) + self.assertIsInstance(val, numpy.ndarray, msg='no monitor') + self.assertEquals(len(val), 0, msg='no monitor') + self.assertEquals(val.dtype, numpy.float64, msg='no monitor') + + + 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) + + val=wf.get(count=wf.nelm) + self.assertEquals(len(val), wf.nelm) + + + 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)) + values.append( value) + + wf.add_callback(onChanges) + write('Added a callback. Now wait for changes...\n') + + t0 = time.time() + while time.time() - t0 < 3: + time.sleep(1.e-4) + if len(values)>0: + break + + self.failUnless(len(values) > 0) + self.assertEquals(len(values[0]),32) + + wf.clear_callbacks() + + + + def test_emptyish_char_waveform_no_monitor(self): + '''a test of a char waveform of length 1 (NORD=1): value "\0" + without using auto_monitor + ''' + with no_simulator_updates(): + zerostr = PV(pvnames.char_arr_pv, auto_monitor=False) + zerostr.wait_for_connection() + + # elem_count = 128, requested count = None, libca returns count = 1 + zerostr.put([0], wait=True) + self.assertEquals(zerostr.get(as_string=True), '') + numpy.testing.assert_array_equal(zerostr.get(as_string=False), [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]) + + # elem_count = 128, requested count = None, libca returns count = 2 + zerostr.put([0, 0], wait=True) + self.assertEquals(zerostr.get(as_string=True), '') + 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" + with using auto_monitor + ''' + with no_simulator_updates(): + zerostr = PV(pvnames.char_arr_pv, auto_monitor=True) + zerostr.wait_for_connection() + + zerostr.put([0], wait=True) + time.sleep(0.2) + + self.assertEquals(zerostr.get(as_string=True), '') + numpy.testing.assert_array_equal(zerostr.get(as_string=False), [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]) + + zerostr.put([0, 0], wait=True) + time.sleep(0.2) + + self.assertEquals(zerostr.get(as_string=True), '') + 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 = get_pv(pvnames.enum_pv) + self.assertIsNot(pv, None) + pv.put('Stop') + time.sleep(0.1) + val = pv.get() + self.assertEqual(val, 0) + + + def test_DoubleVal(self): + pvn = pvnames.double_pv + pv = get_pv(pvn) + pv.get() + cdict = pv.get_ctrlvars() + write( 'Testing CTRL Values for a Double (%s)\n' % (pvn)) + self.failUnless('severity' in cdict) + self.failUnless(len(pv.host) > 1) + self.assertEqual(pv.count,1) + self.assertEqual(pv.precision, pvnames.double_pv_prec) + units= ca.BYTES2STR(pv.units) + self.assertEqual(units, pvnames.double_pv_units) + self.failUnless(pv.access.startswith('read')) + + + def test_type_converions_2(self): + write("CA type conversions arrays\n") + pvlist = (pvnames.char_arr_pv, + pvnames.long_arr_pv, + pvnames.double_arr_pv) + with no_simulator_updates(): + chids = [] + for name in pvlist: + chid = ca.create_channel(name) + ca.connect_channel(chid) + chids.append((chid, name)) + ca.poll(evt=0.025, iot=5.0) + ca.poll(evt=0.05, iot=10.0) + + values = {} + for chid, name in chids: + values[name] = ca.get(chid) + for promotion in ('ctrl', 'time'): + for chid, pvname in chids: + write('=== %s chid=%s as %s\n' % (ca.name(chid), + repr(chid), promotion)) + time.sleep(0.01) + if promotion == 'ctrl': + ntype = ca.promote_type(chid, use_ctrl=True) + else: + ntype = ca.promote_type(chid, use_time=True) + + val = ca.get(chid, ftype=ntype) + cval = ca.get(chid, as_string=True) + for a, b in zip(val, values[pvname]): + self.assertEqual(a, b) + + def test_waveform_get_1elem(self): + 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 = 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) + print('val is', val, type(val)) + self.assertIsInstance(val, numpy.ndarray) + self.assertEqual(len(val), 1) + + val = pv.get(count=1, as_numpy=False, use_monitor=False) + print('val is', val, type(val)) + self.assertIsInstance(val, list) + 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) + + +# chid = ca.create_channel(pvnames.int_pv, +# callback=onConnect) +# +# time.sleep(0.1) diff --git a/tests/pvnames.py b/tests/pvnames.py new file mode 100644 index 0000000..d91286d --- /dev/null +++ b/tests/pvnames.py @@ -0,0 +1,72 @@ +# +# list of local pv names to use for testing + + +#### 1 +# this pv should be a DOUBLE. It will NOT be set, but +# you should provide the host_name, units, and precision. It +# is assumed to have count=1 +double_pv = 'Py:ao1' +double_pv_units = 'microns' +double_pv_prec = 4 + +double_pv2 = 'Py:ao2' + +pause_pv = 'Py:pause' +#### 2 +# this pv should be an ENUM. It will NOT be set. +# provide the names of the ENUM states + +#### Theae are PVs of the various native types +### They will NOT be set. +str_pv = 'Py:ao1.DESC' +int_pv = 'Py:long2' +long_pv = 'Py:long2' +float_pv = 'Py:ao3' +enum_pv = 'Py:mbbo1' +enum_pv_strs = ['Stop', 'Start', 'Pause', 'Resume'] + +proc_pv = 'Py:ao1.PROC' + +## Here are some waveform / array data PVs +long_arr_pv = 'Py:long2k' +double_arr_pv = 'Py:double2k' +string_arr_pv = 'Py:string128' +# char / byte array +char_arr_pv = 'Py:char128' +char_arrays = ['Py:char128', 'Py:char2k', 'Py:char64k'] +long_arrays = ['Py:long128', 'Py:long2k', 'Py:long64k'] +double_arrays = ['Py:double128', 'Py:double2k', 'Py:double64k'] + + +#### +# provide a single motor prefix (to which '.VAL' and '.RBV' etc will be added) + +motor_list = ['sim:mtr%d' % i for i in range(1, 7)] +motor1 = motor_list[0] +motor2 = motor_list[1] + +#### +# Here, provide a PV that changes at least once very 10 seconds +updating_pv1 = 'Py:ao1' +updating_str1 = 'Py:char256' + +#### +# Here, provide a list of PVs that change at least once very 10 seconds +updating_pvlist = ['Py:ao1', 'Py:ai1', 'Py:long1', 'Py:ao2'] +#### alarm test + +non_updating_pv = 'Py:ao4' + +alarm_pv = 'Py:long1' +alarm_comp='ge' +alarm_trippoint = 7 + + +#### subarray test +subarr_driver = 'Py:wave_test' +subarr1 = 'Py:subArr1' +subarr2 = 'Py:subArr2' +subarr3 = 'Py:subArr3' +subarr4 = 'Py:subArr4' +zero_len_subarr1 = 'Py:ZeroLenSubArr1' diff --git a/tests/sg_test.py b/tests/sg_test.py new file mode 100644 index 0000000..3f5c987 --- /dev/null +++ b/tests/sg_test.py @@ -0,0 +1,43 @@ +from __future__ import print_function + +import time +import epics +import pvnames +print('== Test get/put for synchronous groups') + +pvs = pvnames.motor_list + +chids = [epics.ca.create_channel(pvname) for pvname in pvs] + +for chid in chids: + epics.ca.connect_channel(chid) + epics.ca.put(chid, 0) + +print('Now create synch group ') +sg = epics.ca.sg_create() + +data = [epics.ca.sg_get(sg, chid) for chid in chids] + +print('Now change these PVs for the next 10 seconds') +time.sleep(10.0) + +print('Synchronous block:') +epics.ca.sg_block(sg) +print('Done. Values') +for pvname, dat, chid in zip(pvs, data, chids): + print("%s = %s" % (pvname, str( epics.ca._unpack(dat, chid=chid)))) + +epics.ca.sg_reset(sg) + +print('OK, now we will put everything back to 0 synchronously') + +for chid in chids: + epics.ca.sg_put(sg, chid, 0) +print('sg_put done, but not blocked / commited. Sleep for 5 seconds ') +time.sleep(5.0) +print('Now Go: ') +epics.ca.sg_block(sg) +print('done.') + + + diff --git a/tests/test_cas.py b/tests/test_cas.py new file mode 100644 index 0000000..192beba --- /dev/null +++ b/tests/test_cas.py @@ -0,0 +1,180 @@ +import time +import pytest +import subprocess +from tempfile import NamedTemporaryFile as NTF +import epics + + +cas_test_db = ''' + record(ao, "test:ao") { + field(ASG, "rps_threshold") + field(DRVH, "10") + field(DRVL, "0") + } + + record(bo, "test:bo") { + field(ASG, "rps_lock") + field(ZNAM, "OUT") + field(ONAM, "IN") + } + + record(ao, "test:ao2") { + field(DRVH, "5") + field(DRVL, "1") + } + + record(bo, "test:permit") { + field(VAL, "0") + field(PINI, "1") + field(ZNAM, "DISABLED") + field(ONAM, "ENABLED") + } + ''' + +cas_rules = ''' + ASG(DEFAULT) { + RULE(1,READ) + RULE(1,WRITE,TRAPWRITE) + } + + ASG(rps_threshold) { + INPA("$(P):permit") + RULE(1, READ) + RULE(0, WRITE, TRAPWRITE) { + CALC("A=1") + } + RULE(1, WRITE, TRAPWRITE) { + CALC("A=0") + } + } + + ASG(rps_lock) { + INPA("$(P):permit") + RULE(1, READ) + RULE(1, WRITE, TRAPWRITE) { + CALC("A=0") + } + } + ''' + +# use yield_fixture() for compatibility with pytest < 2.10 +@pytest.yield_fixture(scope='module') +def softioc(): + with NTF(mode='w+') as cf, NTF(mode='w+') as df: + cf.write(cas_rules) + cf.flush() + df.write(cas_test_db) + df.flush() + + proc = subprocess.Popen(['softIoc', '-D', + '/home/travis/mc/envs/testenv/epics/dbd/softIoc.dbd', + '-m', 'P=test', '-a', cf.name, + '-d', df.name], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE) + yield proc + + try: + proc.kill() + proc.wait() + except OSError: + pass + + +@pytest.yield_fixture(scope='module') +def pvs(): + pvlist = ['test:ao', 'test:ao.DRVH', 'test:bo', 'test:ao2', + 'test:permit'] + pvs = dict() + for name in pvlist: + pv = epics.get_pv(name) + pv.wait_for_connection() + pvs[pv.pvname] = pv + + yield pvs + for pv in pvs.values(): + pv.disconnect() + + +def test_connected(softioc, pvs): + for pv in pvs.values(): + assert pv.connected + +def test_permit_disabled(softioc, pvs): + # with the permit disabled, all test pvs should be readable/writable + for pv in pvs.values(): + assert pv.read_access and pv.write_access + +def test_permit_enabled(softioc, pvs): + # set the run-permit + pvs['test:permit'].put(1, wait=True) + assert pvs['test:permit'].get(as_string=True, use_monitor=False) == 'ENABLED' + + # rps_lock rule should disable write access + assert pvs['test:bo'].write_access is False + with pytest.raises(epics.ca.CASeverityException): + pvs['test:bo'].put(1, wait=True) + + # rps_threshold rule should disable write access to metadata, not VAL + assert pvs['test:ao'].write_access is True + assert pvs['test:ao.DRVH'].write_access is False + with pytest.raises(epics.ca.CASeverityException): + pvs['test:ao.DRVH'].put(100, wait=True) + +def test_pv_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' + + def lcb(read_access, write_access, pv=None): + assert pv.read_access == read_access + assert pv.write_access == write_access + pv.flag = True + + bo = epics.get_pv('test:bo', access_callback=lcb) + bo.flag = False + + # set the run-permit to trigger an access rights event + pvs['test:permit'].put(1, wait=True) + 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 = epics.ca.create_channel('test:bo') + assert bo_id is not None + + def lcb(read_access, write_access): + assert read_access and write_access + lcb.sentinal = True + + lcb.sentinal = False + 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_install.py b/tests/test_install.py new file mode 100644 index 0000000..b675536 --- /dev/null +++ b/tests/test_install.py @@ -0,0 +1,46 @@ +import pytest +import os +import sys +import re + +import epics + + +# These tests should only be run by Travis CI +pytestmark = pytest.mark.skipif(os.getenv('TRAVIS') is None, + reason='TRAVIS is not defined') + +def test_libca_init(): + assert epics.ca.initialize_libca() + +# Skip if Travis defines NOLIBCA +@pytest.mark.skipif(os.getenv('NOLIBCA') is not None, + reason='NOLIBCA is defined') +def test_install_encapsulation(): + libca = epics.ca.find_libca() + system = sys.platform + + if 'linux' in system: + assert re.search('.*/epics/clibs/.*/libca\.so$', libca) + elif 'darwin' in system: + assert re.search('.*/epics/clibs/.*/libca\.dylib$', libca) + elif 'win' in system: + assert re.search('.*\epics\clibs\.*/ca\\.dll$', libca) + else: + raise 'Are you using BSD? You are hardcore...' + +# Skip if NOLIBCA is not defined +@pytest.mark.skipif(os.getenv('NOLIBCA') is None, + reason='NOLIBCA is not defined') +def test_install_no_libs(): + libca = epics.ca.find_libca() + system = sys.platform + + if 'linux' in system: + assert re.search('.*/epics/clibs/.*/libca\.so$', libca) is None + elif 'darwin' in system: + assert re.search('.*/epics/clibs/.*/libca\.dylib$', libca) is None + elif 'win' in system: + assert re.search('.*\epics\clibs\.*/ca\\.dll$', libca) is None + else: + raise 'Are you using BSD? You are hardcore...' diff --git a/tests/test_multiprocessing.py b/tests/test_multiprocessing.py new file mode 100644 index 0000000..ba11bc0 --- /dev/null +++ b/tests/test_multiprocessing.py @@ -0,0 +1,44 @@ +from __future__ import print_function +import epics +import time +import multiprocessing as mp +import threading + +import pvnames +PVN1 = pvnames.double_pv # 'Py:ao2' +PVN2 = pvnames.double_pv2 # 'Py:ao3' + +def subprocess(*args): + print('==subprocess==', args) + mypvs = [epics.get_pv(pvname) for pvname in args] + + for i in range(10): + time.sleep(0.750) + out = [(p.pvname, p.get(as_string=True)) for p in mypvs] + out = ', '.join(["%s=%s" % o for o in out]) + print('==sub (%d): %s' % (i, out)) + +def main_process(): + def monitor(pvname=None, char_value=None, **kwargs): + print('--main:monitor %s=%s' % (pvname, char_value)) + + print('--main:') + pv1 = epics.get_pv(PVN1) + print('--main:init %s=%s' % (PVN1, pv1.get())) + pv1.add_callback(callback=monitor) + + try: + proc1 = epics.CAProcess(target=subprocess, + args=(PVN1, PVN2)) + proc1.start() + proc1.join() + except KeyboardInterrupt: + print('--main: killing subprocess') + proc1.terminate() + + print('--main: subprocess complete') + time.sleep(0.9) + print('--main:final %s=%s' % (PVN1, pv1.get())) + +if __name__ == '__main__': + main_process() diff --git a/tests/test_pool.py b/tests/test_pool.py new file mode 100644 index 0000000..999f277 --- /dev/null +++ b/tests/test_pool.py @@ -0,0 +1,51 @@ +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] + + +@contextmanager +def pool_ctx(): + pool = epics.CAPool() + yield pool + 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:') + print('\tpool.process =', pool.Process) + values = pool.map(epics.caget, PVS) + + for pv, value in zip(PVS, values): + print('\t%s = %s' % (pv, value)) + + +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 + ''' + with pool_ctx() as pool: + print('Multiprocessing Manager test:') + + manager = mp.Manager() + pv_dict = manager.dict() + + results = [pool.apply_async(_manager_test_fcn, [pv_dict, pv]) + for pv in PVS] + + print('\tResulting pv dictionary: %s' % pv_dict) diff --git a/tests/test_threading.py b/tests/test_threading.py new file mode 100644 index 0000000..6d30b05 --- /dev/null +++ b/tests/test_threading.py @@ -0,0 +1,71 @@ +import epics +import threading +import pvnames + + +def test_basic_thread(): + result = [] + def thread(): + epics.ca.use_initial_context() + pv = epics.get_pv(pvnames.double_pv) + result.append(pv.get()) + + epics.ca.use_initial_context() + t = threading.Thread(target=thread) + t.start() + t.join() + + assert len(result) and result[0] is not None + + +def test_basic_cathread(): + result = [] + def thread(): + pv = epics.get_pv(pvnames.double_pv) + result.append(pv.get()) + + epics.ca.use_initial_context() + t = epics.ca.CAThread(target=thread) + t.start() + t.join() + + assert len(result) and result[0] is not None + + +def test_attach_context(): + result = [] + def thread(): + epics.ca.create_context() + 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.get_pv(pvnames.double_pv) + assert pv.wait_for_connection() + result.append(pv.get()) + + epics.ca.use_initial_context() + ctx = epics.ca.current_context() + t = threading.Thread(target=thread) + t.start() + t.join() + + assert len(result) == 2 and result[0] is not None + print(result) + + +def test_pv_from_main(): + result = [] + def thread(): + result.append(pv.get()) + + epics.ca.use_initial_context() + pv = epics.get_pv(pvnames.double_pv2) + + t = epics.ca.CAThread(target=thread) + t.start() + t.join() + + assert len(result) and result[0] is not None diff --git a/tests/thread_put.py b/tests/thread_put.py new file mode 100644 index 0000000..b355850 --- /dev/null +++ b/tests/thread_put.py @@ -0,0 +1,44 @@ +"""This script tests using EPICS CA and Python threads together + +Based on code from Friedrich Schotte, NIH, modified by Matt Newville +20-Apr-2010 +""" + +import time +from threading import Thread +import epics +import sys +import pvnames + +epics.caput(pvnames.motor1, 0.3) +epics.caput(pvnames.motor2, -1.0) +time.sleep(1.0) +epics.caput(pvnames.motor2, 1.0, wait=True) +sys.stdout.write('done with initial moves.\n') + +def run_test(pvname, target, run_name='thread c'): + sys.stdout.write( ' -> thread "%s"\n' % run_name) + def onChanges(pvname=None, value=None, char_value=None, **kw): + sys.stdout.write(' %s = %s (%s)\n' % (pvname, char_value, run_name)) + epics.ca.context_create() + p = epics.PV(pvname) + sys.stdout.write('Put %s to %.3f (%s)\n' % (pvname, target,run_name)) + p.put(target, wait=True) + sys.stdout.write( 'Done with Thread %s\n' % run_name) + epics.ca.context_destroy() + +epics.ca.show_cache() + +sys.stdout.write( "Run 2 Background Threads doing simultaneous put-with-waits:\n") +th1 = Thread(target=run_test,args=( pvnames.motor1, 0.5, 'A')) +th2 = Thread(target=run_test,args=( pvnames.motor2, .22, 'B')) +th1.start() +th2.start() + +epics.ca.show_cache() +th2.join() +th1.join() + +sys.stdout.write( 'Done.\n') +time.sleep(0.01) +epics.ca.show_cache() diff --git a/tests/thread_put2.py b/tests/thread_put2.py new file mode 100644 index 0000000..84988e1 --- /dev/null +++ b/tests/thread_put2.py @@ -0,0 +1,28 @@ +from __future__ import print_function + +import time +import threading +import epics +import pvnames +def threaded_pvput(pv, value): + "put-with-wait for calling in a thread" + t0 = time.time() + print(' - threaded_pvput starting at ', pv.get()) + pv.put(value, wait=True, timeout=10.0) + print(' - threaded_pvput done (%.3f sec)' % (time.time()-t0)) + +if __name__ == '__main__': + pvname = pvnames.motor2 + target = 0.55 + + pv = epics.PV(pvname) + pv.put(-target, wait=True) + time.sleep(0.5) + + th = threading.Thread(target=threaded_pvput, + args=(pv, target)) + th.start() + th.join() + print('All Done.') + + diff --git a/tests/thread_test.py b/tests/thread_test.py new file mode 100644 index 0000000..da07ac1 --- /dev/null +++ b/tests/thread_test.py @@ -0,0 +1,44 @@ +"""This script tests using EPICS CA and Python threads together +Based on code from Friedrich Schotte, NIH, modified by Matt Newville +19-Apr-2010 +""" +import time +from sys import stdout +from threading import Thread +import epics +from epics.ca import CAThread + +from pvnames import updating_pvlist +pvlist_a = updating_pvlist[:-1] +pvlist_b = updating_pvlist[1:] + +def run_test(runtime=1, pvnames=None, run_name='thread c'): + msg = '-> thread "%s" will run for %.3f sec, monitoring %s\n' + stdout.write(msg % (run_name, runtime, pvnames)) + def onChanges(pvname=None, value=None, char_value=None, **kw): + stdout.write(' %s = %s (%s)\n' % (pvname, char_value, run_name)) + stdout.flush() + + # epics.ca.use_initial_context() # epics.ca.create_context() + start_time = time.time() + pvs = [epics.PV(pvn, callback=onChanges) for pvn in pvnames] + + while time.time()-start_time < runtime: + time.sleep(0.1) + + [p.clear_callbacks() for p in pvs] + stdout.write( 'Completed Thread %s\n' % ( run_name)) + +stdout.write( "First, create a PV in the main thread:\n") +p = epics.PV(updating_pvlist[0]) + +stdout.write("Run 2 Background Threads simultaneously:\n") +th1 = CAThread(target=run_test,args=(3, pvlist_a, 'A')) +th1.start() + +th2 = CAThread(target=run_test,args=(6, pvlist_b, 'B')) +th2.start() + +th2.join() +th1.join() +stdout.write('Done\n') diff --git a/tests/thread_test_BNLt2.py b/tests/thread_test_BNLt2.py new file mode 100644 index 0000000..bb78105 --- /dev/null +++ b/tests/thread_test_BNLt2.py @@ -0,0 +1,43 @@ + +import time +from sys import stdout +from threading import Thread +import epics +from epics.ca import CAThread + + +pvlist_a = ('S14A:P0:mswAve:x:AdjustedCC', + 'S14A:P0:ms:x:SetpointAO', + 'S13C:P0:mswAve:x:AdjustedCC', + 'S13C:P0:ms:x:SetpointAO', + 'S13C:P0:mswAve:x:ErrorCC', + 'S13B:P0:mswAve:x:AdjustedCC', + 'S13B:P0:ms:x:SetpointAO', + 'S13B:P0:mswAve:x:ErrorCC') + +pvlist_b = ('S13B:P0:mswAve:x:AdjustedCC', + 'S13B:P0:ms:x:SetpointAO', + 'S13B:P0:mswAve:x:ErrorCC', + 'S13ds:ID:SrcPt:xAngleM', + 'S13ds:ID:SrcPt:xPositionM', + 'S13ds:ID:SrcPt:yAngleM', + 'S13ds:ID:SrcPt:yPositionM') + + +def onChanges(pvname=None, value=None, char_value=None, host=None, **kws): + print(' %s = %s / %s ' % (pvname, char_value, host)) + +pvs = [] +for pvname in pvlist_a + pvlist_b: + pvs.append(epics.PV(pvname)) + +t0 = time.time() +while time.time()-t0 < 60: + try: + time.sleep(0.1) + for pv in pvs: + x = (pv.pvname, pv.get(as_string=True)) + except KeyboardInterrupt: + break + +print('Done') diff --git a/tests/thread_test_BNLtt.py b/tests/thread_test_BNLtt.py new file mode 100644 index 0000000..5a341e6 --- /dev/null +++ b/tests/thread_test_BNLtt.py @@ -0,0 +1,60 @@ + +import time +from sys import stdout +from threading import Thread +import epics +from epics.ca import CAThread + + +pvlist_a = ('S14A:P0:mswAve:x:AdjustedCC', + 'S14A:P0:ms:x:SetpointAO', + 'S13C:P0:mswAve:x:AdjustedCC', + 'S13C:P0:ms:x:SetpointAO', + 'S13C:P0:mswAve:x:ErrorCC', + 'S13B:P0:mswAve:x:AdjustedCC', + 'S13B:P0:ms:x:SetpointAO', + 'S13B:P0:mswAve:x:ErrorCC') + +pvlist_b = ('S13B:P0:mswAve:x:AdjustedCC', + 'S13B:P0:ms:x:SetpointAO', + 'S13B:P0:mswAve:x:ErrorCC', + 'S13ds:ID:SrcPt:xAngleM', + 'S13ds:ID:SrcPt:xPositionM', + 'S13ds:ID:SrcPt:yAngleM', + 'S13ds:ID:SrcPt:yPositionM') + + +def run_test(runtime=1, pvnames=None, run_name='thread c'): + msg = '-> thread "%s" will run for %.3f sec, monitoring %s\n' + stdout.write(msg % (run_name, runtime, pvnames)) + def onChanges(pvname=None, value=None, char_value=None, **kw): + stdout.write(' %s = %s (%s)\n' % (pvname, char_value, run_name)) + stdout.flush() + + # epics.ca.use_initial_context() # epics.ca.create_context() + start_time = time.time() + pvs = [epics.PV(pvn, callback=onChanges) for pvn in pvnames] + + while time.time()-start_time < runtime: + time.sleep(0.001) + + [p.clear_callbacks() for p in pvs] + stdout.write( 'Completed Thread %s\n' % ( run_name)) + +stdout.write( "First, create a PV in the main thread:\n") +for pvname in pvlist_a + pvlist_b: + p = epics.PV(pvname) + p.connect() + p.get() + print(p.info) + +stdout.write("Run 2 Background Threads simultaneously:\n") +th1 = CAThread(target=run_test,args=(30, pvlist_a, 'A')) +th1.start() + +th2 = CAThread(target=run_test,args=(60, pvlist_b, 'B')) +th2.start() + +th2.join() +th1.join() +stdout.write('Done\n') diff --git a/versioneer.py b/versioneer.py new file mode 100644 index 0000000..64fea1c --- /dev/null +++ b/versioneer.py @@ -0,0 +1,1822 @@ + +# Version: 0.18 + +"""The Versioneer - like a rocketeer, but for versions. + +The Versioneer +============== + +* like a rocketeer, but for versions! +* https://github.com/warner/python-versioneer +* Brian Warner +* License: Public Domain +* Compatible With: python2.6, 2.7, 3.2, 3.3, 3.4, 3.5, 3.6, and pypy +* [![Latest Version] +(https://pypip.in/version/versioneer/badge.svg?style=flat) +](https://pypi.python.org/pypi/versioneer/) +* [![Build Status] +(https://travis-ci.org/warner/python-versioneer.png?branch=master) +](https://travis-ci.org/warner/python-versioneer) + +This is a tool for managing a recorded version number in distutils-based +python projects. The goal is to remove the tedious and error-prone "update +the embedded version string" step from your release process. Making a new +release should be as easy as recording a new tag in your version-control +system, and maybe making new tarballs. + + +## Quick Install + +* `pip install versioneer` to somewhere to your $PATH +* add a `[versioneer]` section to your setup.cfg (see below) +* run `versioneer install` in your source tree, commit the results + +## Version Identifiers + +Source trees come from a variety of places: + +* a version-control system checkout (mostly used by developers) +* a nightly tarball, produced by build automation +* a snapshot tarball, produced by a web-based VCS browser, like github's + "tarball from tag" feature +* a release tarball, produced by "setup.py sdist", distributed through PyPI + +Within each source tree, the version identifier (either a string or a number, +this tool is format-agnostic) can come from a variety of places: + +* ask the VCS tool itself, e.g. "git describe" (for checkouts), which knows + about recent "tags" and an absolute revision-id +* the name of the directory into which the tarball was unpacked +* an expanded VCS keyword ($Id$, etc) +* a `_version.py` created by some earlier build step + +For released software, the version identifier is closely related to a VCS +tag. Some projects use tag names that include more than just the version +string (e.g. "myproject-1.2" instead of just "1.2"), in which case the tool +needs to strip the tag prefix to extract the version identifier. For +unreleased software (between tags), the version identifier should provide +enough information to help developers recreate the same tree, while also +giving them an idea of roughly how old the tree is (after version 1.2, before +version 1.3). Many VCS systems can report a description that captures this, +for example `git describe --tags --dirty --always` reports things like +"0.7-1-g574ab98-dirty" to indicate that the checkout is one revision past the +0.7 tag, has a unique revision id of "574ab98", and is "dirty" (it has +uncommitted changes. + +The version identifier is used for multiple purposes: + +* to allow the module to self-identify its version: `myproject.__version__` +* to choose a name and prefix for a 'setup.py sdist' tarball + +## Theory of Operation + +Versioneer works by adding a special `_version.py` file into your source +tree, where your `__init__.py` can import it. This `_version.py` knows how to +dynamically ask the VCS tool for version information at import time. + +`_version.py` also contains `$Revision$` markers, and the installation +process marks `_version.py` to have this marker rewritten with a tag name +during the `git archive` command. As a result, generated tarballs will +contain enough information to get the proper version. + +To allow `setup.py` to compute a version too, a `versioneer.py` is added to +the top level of your source tree, next to `setup.py` and the `setup.cfg` +that configures it. This overrides several distutils/setuptools commands to +compute the version when invoked, and changes `setup.py build` and `setup.py +sdist` to replace `_version.py` with a small static file that contains just +the generated version data. + +## Installation + +See [INSTALL.md](./INSTALL.md) for detailed installation instructions. + +## Version-String Flavors + +Code which uses Versioneer can learn about its version string at runtime by +importing `_version` from your main `__init__.py` file and running the +`get_versions()` function. From the "outside" (e.g. in `setup.py`), you can +import the top-level `versioneer.py` and run `get_versions()`. + +Both functions return a dictionary with different flavors of version +information: + +* `['version']`: A condensed version string, rendered using the selected + style. This is the most commonly used value for the project's version + string. The default "pep440" style yields strings like `0.11`, + `0.11+2.g1076c97`, or `0.11+2.g1076c97.dirty`. See the "Styles" section + below for alternative styles. + +* `['full-revisionid']`: detailed revision identifier. For Git, this is the + full SHA1 commit id, e.g. "1076c978a8d3cfc70f408fe5974aa6c092c949ac". + +* `['date']`: Date and time of the latest `HEAD` commit. For Git, it is the + commit date in ISO 8601 format. This will be None if the date is not + available. + +* `['dirty']`: a boolean, True if the tree has uncommitted changes. Note that + this is only accurate if run in a VCS checkout, otherwise it is likely to + be False or None + +* `['error']`: if the version string could not be computed, this will be set + to a string describing the problem, otherwise it will be None. It may be + useful to throw an exception in setup.py if this is set, to avoid e.g. + creating tarballs with a version string of "unknown". + +Some variants are more useful than others. Including `full-revisionid` in a +bug report should allow developers to reconstruct the exact code being tested +(or indicate the presence of local changes that should be shared with the +developers). `version` is suitable for display in an "about" box or a CLI +`--version` output: it can be easily compared against release notes and lists +of bugs fixed in various releases. + +The installer adds the following text to your `__init__.py` to place a basic +version in `YOURPROJECT.__version__`: + + from ._version import get_versions + __version__ = get_versions()['version'] + del get_versions + +## Styles + +The setup.cfg `style=` configuration controls how the VCS information is +rendered into a version string. + +The default style, "pep440", produces a PEP440-compliant string, equal to the +un-prefixed tag name for actual releases, and containing an additional "local +version" section with more detail for in-between builds. For Git, this is +TAG[+DISTANCE.gHEX[.dirty]] , using information from `git describe --tags +--dirty --always`. For example "0.11+2.g1076c97.dirty" indicates that the +tree is like the "1076c97" commit but has uncommitted changes (".dirty"), and +that this commit is two revisions ("+2") beyond the "0.11" tag. For released +software (exactly equal to a known tag), the identifier will only contain the +stripped tag, e.g. "0.11". + +Other styles are available. See [details.md](details.md) in the Versioneer +source tree for descriptions. + +## Debugging + +Versioneer tries to avoid fatal errors: if something goes wrong, it will tend +to return a version of "0+unknown". To investigate the problem, run `setup.py +version`, which will run the version-lookup code in a verbose mode, and will +display the full contents of `get_versions()` (including the `error` string, +which may help identify what went wrong). + +## Known Limitations + +Some situations are known to cause problems for Versioneer. This details the +most significant ones. More can be found on Github +[issues page](https://github.com/warner/python-versioneer/issues). + +### Subprojects + +Versioneer has limited support for source trees in which `setup.py` is not in +the root directory (e.g. `setup.py` and `.git/` are *not* siblings). The are +two common reasons why `setup.py` might not be in the root: + +* Source trees which contain multiple subprojects, such as + [Buildbot](https://github.com/buildbot/buildbot), which contains both + "master" and "slave" subprojects, each with their own `setup.py`, + `setup.cfg`, and `tox.ini`. Projects like these produce multiple PyPI + distributions (and upload multiple independently-installable tarballs). +* Source trees whose main purpose is to contain a C library, but which also + provide bindings to Python (and perhaps other langauges) in subdirectories. + +Versioneer will look for `.git` in parent directories, and most operations +should get the right version string. However `pip` and `setuptools` have bugs +and implementation details which frequently cause `pip install .` from a +subproject directory to fail to find a correct version string (so it usually +defaults to `0+unknown`). + +`pip install --editable .` should work correctly. `setup.py install` might +work too. + +Pip-8.1.1 is known to have this problem, but hopefully it will get fixed in +some later version. + +[Bug #38](https://github.com/warner/python-versioneer/issues/38) is tracking +this issue. The discussion in +[PR #61](https://github.com/warner/python-versioneer/pull/61) describes the +issue from the Versioneer side in more detail. +[pip PR#3176](https://github.com/pypa/pip/pull/3176) and +[pip PR#3615](https://github.com/pypa/pip/pull/3615) contain work to improve +pip to let Versioneer work correctly. + +Versioneer-0.16 and earlier only looked for a `.git` directory next to the +`setup.cfg`, so subprojects were completely unsupported with those releases. + +### Editable installs with setuptools <= 18.5 + +`setup.py develop` and `pip install --editable .` allow you to install a +project into a virtualenv once, then continue editing the source code (and +test) without re-installing after every change. + +"Entry-point scripts" (`setup(entry_points={"console_scripts": ..})`) are a +convenient way to specify executable scripts that should be installed along +with the python package. + +These both work as expected when using modern setuptools. When using +setuptools-18.5 or earlier, however, certain operations will cause +`pkg_resources.DistributionNotFound` errors when running the entrypoint +script, which must be resolved by re-installing the package. This happens +when the install happens with one version, then the egg_info data is +regenerated while a different version is checked out. Many setup.py commands +cause egg_info to be rebuilt (including `sdist`, `wheel`, and installing into +a different virtualenv), so this can be surprising. + +[Bug #83](https://github.com/warner/python-versioneer/issues/83) describes +this one, but upgrading to a newer version of setuptools should probably +resolve it. + +### Unicode version strings + +While Versioneer works (and is continually tested) with both Python 2 and +Python 3, it is not entirely consistent with bytes-vs-unicode distinctions. +Newer releases probably generate unicode version strings on py2. It's not +clear that this is wrong, but it may be surprising for applications when then +write these strings to a network connection or include them in bytes-oriented +APIs like cryptographic checksums. + +[Bug #71](https://github.com/warner/python-versioneer/issues/71) investigates +this question. + + +## Updating Versioneer + +To upgrade your project to a new release of Versioneer, do the following: + +* install the new Versioneer (`pip install -U versioneer` or equivalent) +* edit `setup.cfg`, if necessary, to include any new configuration settings + indicated by the release notes. See [UPGRADING](./UPGRADING.md) for details. +* re-run `versioneer install` in your source tree, to replace + `SRC/_version.py` +* commit any changed files + +## Future Directions + +This tool is designed to make it easily extended to other version-control +systems: all VCS-specific components are in separate directories like +src/git/ . The top-level `versioneer.py` script is assembled from these +components by running make-versioneer.py . In the future, make-versioneer.py +will take a VCS name as an argument, and will construct a version of +`versioneer.py` that is specific to the given VCS. It might also take the +configuration arguments that are currently provided manually during +installation by editing setup.py . Alternatively, it might go the other +direction and include code from all supported VCS systems, reducing the +number of intermediate scripts. + + +## License + +To make Versioneer easier to embed, all its code is dedicated to the public +domain. The `_version.py` that it creates is also in the public domain. +Specifically, both are released under the Creative Commons "Public Domain +Dedication" license (CC0-1.0), as described in +https://creativecommons.org/publicdomain/zero/1.0/ . + +""" + +from __future__ import print_function +try: + import configparser +except ImportError: + import ConfigParser as configparser +import errno +import json +import os +import re +import subprocess +import sys + + +class VersioneerConfig: + """Container for Versioneer configuration parameters.""" + + +def get_root(): + """Get the project root directory. + + We require that all commands are run from the project root, i.e. the + directory that contains setup.py, setup.cfg, and versioneer.py . + """ + root = os.path.realpath(os.path.abspath(os.getcwd())) + setup_py = os.path.join(root, "setup.py") + versioneer_py = os.path.join(root, "versioneer.py") + if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)): + # allow 'python path/to/setup.py COMMAND' + root = os.path.dirname(os.path.realpath(os.path.abspath(sys.argv[0]))) + setup_py = os.path.join(root, "setup.py") + versioneer_py = os.path.join(root, "versioneer.py") + if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)): + err = ("Versioneer was unable to run the project root directory. " + "Versioneer requires setup.py to be executed from " + "its immediate directory (like 'python setup.py COMMAND'), " + "or in a way that lets it use sys.argv[0] to find the root " + "(like 'python path/to/setup.py COMMAND').") + raise VersioneerBadRootError(err) + try: + # Certain runtime workflows (setup.py install/develop in a setuptools + # tree) execute all dependencies in a single python process, so + # "versioneer" may be imported multiple times, and python's shared + # module-import table will cache the first one. So we can't use + # os.path.dirname(__file__), as that will find whichever + # versioneer.py was first imported, even in later projects. + me = os.path.realpath(os.path.abspath(__file__)) + me_dir = os.path.normcase(os.path.splitext(me)[0]) + vsr_dir = os.path.normcase(os.path.splitext(versioneer_py)[0]) + if me_dir != vsr_dir: + print("Warning: build in %s is using versioneer.py from %s" + % (os.path.dirname(me), versioneer_py)) + except NameError: + pass + return root + + +def get_config_from_root(root): + """Read the project setup.cfg file to determine Versioneer config.""" + # This might raise EnvironmentError (if setup.cfg is missing), or + # configparser.NoSectionError (if it lacks a [versioneer] section), or + # configparser.NoOptionError (if it lacks "VCS="). See the docstring at + # the top of versioneer.py for instructions on writing your setup.cfg . + setup_cfg = os.path.join(root, "setup.cfg") + parser = configparser.SafeConfigParser() + with open(setup_cfg, "r") as f: + parser.readfp(f) + VCS = parser.get("versioneer", "VCS") # mandatory + + def get(parser, name): + if parser.has_option("versioneer", name): + return parser.get("versioneer", name) + return None + cfg = VersioneerConfig() + cfg.VCS = VCS + cfg.style = get(parser, "style") or "" + cfg.versionfile_source = get(parser, "versionfile_source") + cfg.versionfile_build = get(parser, "versionfile_build") + cfg.tag_prefix = get(parser, "tag_prefix") + if cfg.tag_prefix in ("''", '""'): + cfg.tag_prefix = "" + cfg.parentdir_prefix = get(parser, "parentdir_prefix") + cfg.verbose = get(parser, "verbose") + return cfg + + +class NotThisMethod(Exception): + """Exception raised if a method is not valid for the current scenario.""" + + +# these dictionaries contain VCS-specific tools +LONG_VERSION_PY = {} +HANDLERS = {} + + +def register_vcs_handler(vcs, method): # decorator + """Decorator to mark a method as the handler for a particular VCS.""" + def decorate(f): + """Store f in HANDLERS[vcs][method].""" + if vcs not in HANDLERS: + HANDLERS[vcs] = {} + HANDLERS[vcs][method] = f + return f + return decorate + + +def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, + env=None): + """Call the given command(s).""" + assert isinstance(commands, list) + p = None + for c in commands: + try: + dispcmd = str([c] + args) + # remember shell=False, so use git.cmd on windows, not just git + p = subprocess.Popen([c] + args, cwd=cwd, env=env, + stdout=subprocess.PIPE, + stderr=(subprocess.PIPE if hide_stderr + else None)) + break + except EnvironmentError: + e = sys.exc_info()[1] + if e.errno == errno.ENOENT: + continue + if verbose: + print("unable to run %s" % dispcmd) + print(e) + return None, None + else: + if verbose: + print("unable to find command, tried %s" % (commands,)) + return None, None + stdout = p.communicate()[0].strip() + if sys.version_info[0] >= 3: + stdout = stdout.decode() + if p.returncode != 0: + if verbose: + print("unable to run %s (error)" % dispcmd) + print("stdout was %s" % stdout) + return None, p.returncode + return stdout, p.returncode + + +LONG_VERSION_PY['git'] = ''' +# This file helps to compute a version number in source trees obtained from +# git-archive tarball (such as those provided by githubs download-from-tag +# feature). Distribution tarballs (built by setup.py sdist) and build +# directories (produced by setup.py build) will contain a much shorter file +# that just contains the computed version number. + +# This file is released into the public domain. Generated by +# versioneer-0.18 (https://github.com/warner/python-versioneer) + +"""Git implementation of _version.py.""" + +import errno +import os +import re +import subprocess +import sys + + +def get_keywords(): + """Get the keywords needed to look up the version information.""" + # these strings will be replaced by git during git-archive. + # 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 = "%(DOLLAR)sFormat:%%d%(DOLLAR)s" + git_full = "%(DOLLAR)sFormat:%%H%(DOLLAR)s" + git_date = "%(DOLLAR)sFormat:%%ci%(DOLLAR)s" + keywords = {"refnames": git_refnames, "full": git_full, "date": git_date} + return keywords + + +class VersioneerConfig: + """Container for Versioneer configuration parameters.""" + + +def get_config(): + """Create, populate and return the VersioneerConfig() object.""" + # these strings are filled in when 'setup.py versioneer' creates + # _version.py + cfg = VersioneerConfig() + cfg.VCS = "git" + cfg.style = "%(STYLE)s" + cfg.tag_prefix = "%(TAG_PREFIX)s" + cfg.parentdir_prefix = "%(PARENTDIR_PREFIX)s" + cfg.versionfile_source = "%(VERSIONFILE_SOURCE)s" + cfg.verbose = False + return cfg + + +class NotThisMethod(Exception): + """Exception raised if a method is not valid for the current scenario.""" + + +LONG_VERSION_PY = {} +HANDLERS = {} + + +def register_vcs_handler(vcs, method): # decorator + """Decorator to mark a method as the handler for a particular VCS.""" + def decorate(f): + """Store f in HANDLERS[vcs][method].""" + if vcs not in HANDLERS: + HANDLERS[vcs] = {} + HANDLERS[vcs][method] = f + return f + return decorate + + +def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, + env=None): + """Call the given command(s).""" + assert isinstance(commands, list) + p = None + for c in commands: + try: + dispcmd = str([c] + args) + # remember shell=False, so use git.cmd on windows, not just git + p = subprocess.Popen([c] + args, cwd=cwd, env=env, + stdout=subprocess.PIPE, + stderr=(subprocess.PIPE if hide_stderr + else None)) + break + except EnvironmentError: + e = sys.exc_info()[1] + if e.errno == errno.ENOENT: + continue + if verbose: + print("unable to run %%s" %% dispcmd) + print(e) + return None, None + else: + if verbose: + print("unable to find command, tried %%s" %% (commands,)) + return None, None + stdout = p.communicate()[0].strip() + if sys.version_info[0] >= 3: + stdout = stdout.decode() + if p.returncode != 0: + if verbose: + print("unable to run %%s (error)" %% dispcmd) + print("stdout was %%s" %% stdout) + return None, p.returncode + return stdout, p.returncode + + +def versions_from_parentdir(parentdir_prefix, root, verbose): + """Try to determine the version from the parent directory name. + + Source tarballs conventionally unpack into a directory that includes both + the project name and a version string. We will also support searching up + two directory levels for an appropriately named parent directory + """ + rootdirs = [] + + for i in range(3): + dirname = os.path.basename(root) + if dirname.startswith(parentdir_prefix): + return {"version": dirname[len(parentdir_prefix):], + "full-revisionid": None, + "dirty": False, "error": None, "date": None} + else: + rootdirs.append(root) + root = os.path.dirname(root) # up a level + + if verbose: + print("Tried directories %%s but none started with prefix %%s" %% + (str(rootdirs), parentdir_prefix)) + raise NotThisMethod("rootdir doesn't start with parentdir_prefix") + + +@register_vcs_handler("git", "get_keywords") +def git_get_keywords(versionfile_abs): + """Extract version information from the given file.""" + # the code embedded in _version.py can just fetch the value of these + # keywords. When used from setup.py, we don't want to import _version.py, + # so we do it with a regexp instead. This function is not used from + # _version.py. + keywords = {} + try: + f = open(versionfile_abs, "r") + for line in f.readlines(): + if line.strip().startswith("git_refnames ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["refnames"] = mo.group(1) + if line.strip().startswith("git_full ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["full"] = mo.group(1) + if line.strip().startswith("git_date ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["date"] = mo.group(1) + f.close() + except EnvironmentError: + pass + return keywords + + +@register_vcs_handler("git", "keywords") +def git_versions_from_keywords(keywords, tag_prefix, verbose): + """Get version information from git keywords.""" + if not keywords: + raise NotThisMethod("no keywords at all, weird") + date = keywords.get("date") + if date is not None: + # git-2.2.0 added "%%cI", which expands to an ISO-8601 -compliant + # datestamp. However we prefer "%%ci" (which expands to an "ISO-8601 + # -like" string, which we must then edit to make compliant), because + # it's been around since git-1.5.3, and it's too difficult to + # discover which version we're using, or to work around using an + # older one. + date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) + refnames = keywords["refnames"].strip() + if refnames.startswith("$Format"): + if verbose: + print("keywords are unexpanded, not using") + raise NotThisMethod("unexpanded keywords, not a git-archive tarball") + refs = set([r.strip() for r in refnames.strip("()").split(",")]) + # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of + # just "foo-1.0". If we see a "tag: " prefix, prefer those. + TAG = "tag: " + tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) + if not tags: + # Either we're using git < 1.8.3, or there really are no tags. We use + # a heuristic: assume all version tags have a digit. The old git %%d + # expansion behaves like git log --decorate=short and strips out the + # refs/heads/ and refs/tags/ prefixes that would let us distinguish + # between branches and tags. By ignoring refnames without digits, we + # filter out many common branch names like "release" and + # "stabilization", as well as "HEAD" and "master". + tags = set([r for r in refs if re.search(r'\d', r)]) + if verbose: + print("discarding '%%s', no digits" %% ",".join(refs - tags)) + if verbose: + print("likely tags: %%s" %% ",".join(sorted(tags))) + for ref in sorted(tags): + # sorting will prefer e.g. "2.0" over "2.0rc1" + if ref.startswith(tag_prefix): + r = ref[len(tag_prefix):] + if verbose: + print("picking %%s" %% r) + return {"version": r, + "full-revisionid": keywords["full"].strip(), + "dirty": False, "error": None, + "date": date} + # no suitable tags, so version is "0+unknown", but full hex is still there + if verbose: + print("no suitable tags, using unknown + full revision id") + return {"version": "0+unknown", + "full-revisionid": keywords["full"].strip(), + "dirty": False, "error": "no suitable tags", "date": None} + + +@register_vcs_handler("git", "pieces_from_vcs") +def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): + """Get version from 'git describe' in the root of the source tree. + + This only gets called if the git-archive 'subst' keywords were *not* + expanded, and _version.py hasn't already been rewritten with a short + version string, meaning we're inside a checked out source tree. + """ + GITS = ["git"] + if sys.platform == "win32": + GITS = ["git.cmd", "git.exe"] + + out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, + hide_stderr=True) + if rc != 0: + if verbose: + print("Directory %%s not under git control" %% root) + raise NotThisMethod("'git rev-parse --git-dir' returned error") + + # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] + # if there isn't one, this yields HEX[-dirty] (no NUM) + describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty", + "--always", "--long", + "--match", "%%s*" %% tag_prefix], + cwd=root) + # --long was added in git-1.5.5 + if describe_out is None: + raise NotThisMethod("'git describe' failed") + describe_out = describe_out.strip() + full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) + if full_out is None: + raise NotThisMethod("'git rev-parse' failed") + full_out = full_out.strip() + + pieces = {} + pieces["long"] = full_out + pieces["short"] = full_out[:7] # maybe improved later + pieces["error"] = None + + # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] + # TAG might have hyphens. + git_describe = describe_out + + # look for -dirty suffix + dirty = git_describe.endswith("-dirty") + pieces["dirty"] = dirty + if dirty: + git_describe = git_describe[:git_describe.rindex("-dirty")] + + # now we have TAG-NUM-gHEX or HEX + + if "-" in git_describe: + # TAG-NUM-gHEX + mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) + if not mo: + # unparseable. Maybe git-describe is misbehaving? + pieces["error"] = ("unable to parse git-describe output: '%%s'" + %% describe_out) + return pieces + + # tag + full_tag = mo.group(1) + if not full_tag.startswith(tag_prefix): + if verbose: + fmt = "tag '%%s' doesn't start with prefix '%%s'" + print(fmt %% (full_tag, tag_prefix)) + pieces["error"] = ("tag '%%s' doesn't start with prefix '%%s'" + %% (full_tag, tag_prefix)) + return pieces + pieces["closest-tag"] = full_tag[len(tag_prefix):] + + # distance: number of commits since tag + pieces["distance"] = int(mo.group(2)) + + # commit: short hex revision ID + pieces["short"] = mo.group(3) + + else: + # HEX: no tags + pieces["closest-tag"] = None + count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], + cwd=root) + pieces["distance"] = int(count_out) # total number of commits + + # commit date: see ISO-8601 comment in git_versions_from_keywords() + date = run_command(GITS, ["show", "-s", "--format=%%ci", "HEAD"], + cwd=root)[0].strip() + pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) + + return pieces + + +def plus_or_dot(pieces): + """Return a + if we don't already have one, else return a .""" + if "+" in pieces.get("closest-tag", ""): + return "." + return "+" + + +def render_pep440(pieces): + """Build up version string, with post-release "local version identifier". + + Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you + get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty + + Exceptions: + 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += plus_or_dot(pieces) + rendered += "%%d.g%%s" %% (pieces["distance"], pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + else: + # exception #1 + rendered = "0+untagged.%%d.g%%s" %% (pieces["distance"], + pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + return rendered + + +def render_pep440_pre(pieces): + """TAG[.post.devDISTANCE] -- No -dirty. + + Exceptions: + 1: no tags. 0.post.devDISTANCE + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"]: + rendered += ".post.dev%%d" %% pieces["distance"] + else: + # exception #1 + rendered = "0.post.dev%%d" %% pieces["distance"] + return rendered + + +def render_pep440_post(pieces): + """TAG[.postDISTANCE[.dev0]+gHEX] . + + The ".dev0" means dirty. Note that .dev0 sorts backwards + (a dirty tree will appear "older" than the corresponding clean one), + but you shouldn't be releasing software with -dirty anyways. + + Exceptions: + 1: no tags. 0.postDISTANCE[.dev0] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += ".post%%d" %% pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + rendered += plus_or_dot(pieces) + rendered += "g%%s" %% pieces["short"] + else: + # exception #1 + rendered = "0.post%%d" %% pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + rendered += "+g%%s" %% pieces["short"] + return rendered + + +def render_pep440_old(pieces): + """TAG[.postDISTANCE[.dev0]] . + + The ".dev0" means dirty. + + Eexceptions: + 1: no tags. 0.postDISTANCE[.dev0] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += ".post%%d" %% pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + else: + # exception #1 + rendered = "0.post%%d" %% pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + return rendered + + +def render_git_describe(pieces): + """TAG[-DISTANCE-gHEX][-dirty]. + + Like 'git describe --tags --dirty --always'. + + Exceptions: + 1: no tags. HEX[-dirty] (note: no 'g' prefix) + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"]: + rendered += "-%%d-g%%s" %% (pieces["distance"], pieces["short"]) + else: + # exception #1 + rendered = pieces["short"] + if pieces["dirty"]: + rendered += "-dirty" + return rendered + + +def render_git_describe_long(pieces): + """TAG-DISTANCE-gHEX[-dirty]. + + Like 'git describe --tags --dirty --always -long'. + The distance/hash is unconditional. + + Exceptions: + 1: no tags. HEX[-dirty] (note: no 'g' prefix) + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + rendered += "-%%d-g%%s" %% (pieces["distance"], pieces["short"]) + else: + # exception #1 + rendered = pieces["short"] + if pieces["dirty"]: + rendered += "-dirty" + return rendered + + +def render(pieces, style): + """Render the given version pieces into the requested style.""" + if pieces["error"]: + return {"version": "unknown", + "full-revisionid": pieces.get("long"), + "dirty": None, + "error": pieces["error"], + "date": None} + + if not style or style == "default": + style = "pep440" # the default + + if style == "pep440": + rendered = render_pep440(pieces) + elif style == "pep440-pre": + rendered = render_pep440_pre(pieces) + elif style == "pep440-post": + rendered = render_pep440_post(pieces) + elif style == "pep440-old": + rendered = render_pep440_old(pieces) + elif style == "git-describe": + rendered = render_git_describe(pieces) + elif style == "git-describe-long": + rendered = render_git_describe_long(pieces) + else: + raise ValueError("unknown style '%%s'" %% style) + + return {"version": rendered, "full-revisionid": pieces["long"], + "dirty": pieces["dirty"], "error": None, + "date": pieces.get("date")} + + +def get_versions(): + """Get version information or return default if unable to do so.""" + # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have + # __file__, we can work backwards from there to the root. Some + # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which + # case we can only use expanded keywords. + + cfg = get_config() + verbose = cfg.verbose + + try: + return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, + verbose) + except NotThisMethod: + pass + + try: + root = os.path.realpath(__file__) + # versionfile_source is the relative path from the top of the source + # tree (where the .git directory might live) to this file. Invert + # this to find the root from __file__. + for i in cfg.versionfile_source.split('/'): + root = os.path.dirname(root) + except NameError: + return {"version": "0+unknown", "full-revisionid": None, + "dirty": None, + "error": "unable to find root of source tree", + "date": None} + + try: + pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) + return render(pieces, cfg.style) + except NotThisMethod: + pass + + try: + if cfg.parentdir_prefix: + return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) + except NotThisMethod: + pass + + return {"version": "0+unknown", "full-revisionid": None, + "dirty": None, + "error": "unable to compute version", "date": None} +''' + + +@register_vcs_handler("git", "get_keywords") +def git_get_keywords(versionfile_abs): + """Extract version information from the given file.""" + # the code embedded in _version.py can just fetch the value of these + # keywords. When used from setup.py, we don't want to import _version.py, + # so we do it with a regexp instead. This function is not used from + # _version.py. + keywords = {} + try: + f = open(versionfile_abs, "r") + for line in f.readlines(): + if line.strip().startswith("git_refnames ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["refnames"] = mo.group(1) + if line.strip().startswith("git_full ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["full"] = mo.group(1) + if line.strip().startswith("git_date ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["date"] = mo.group(1) + f.close() + except EnvironmentError: + pass + return keywords + + +@register_vcs_handler("git", "keywords") +def git_versions_from_keywords(keywords, tag_prefix, verbose): + """Get version information from git keywords.""" + if not keywords: + raise NotThisMethod("no keywords at all, weird") + date = keywords.get("date") + if date is not None: + # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant + # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 + # -like" string, which we must then edit to make compliant), because + # it's been around since git-1.5.3, and it's too difficult to + # discover which version we're using, or to work around using an + # older one. + date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) + refnames = keywords["refnames"].strip() + if refnames.startswith("$Format"): + if verbose: + print("keywords are unexpanded, not using") + raise NotThisMethod("unexpanded keywords, not a git-archive tarball") + refs = set([r.strip() for r in refnames.strip("()").split(",")]) + # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of + # just "foo-1.0". If we see a "tag: " prefix, prefer those. + TAG = "tag: " + tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) + if not tags: + # Either we're using git < 1.8.3, or there really are no tags. We use + # a heuristic: assume all version tags have a digit. The old git %d + # expansion behaves like git log --decorate=short and strips out the + # refs/heads/ and refs/tags/ prefixes that would let us distinguish + # between branches and tags. By ignoring refnames without digits, we + # filter out many common branch names like "release" and + # "stabilization", as well as "HEAD" and "master". + tags = set([r for r in refs if re.search(r'\d', r)]) + if verbose: + print("discarding '%s', no digits" % ",".join(refs - tags)) + if verbose: + print("likely tags: %s" % ",".join(sorted(tags))) + for ref in sorted(tags): + # sorting will prefer e.g. "2.0" over "2.0rc1" + if ref.startswith(tag_prefix): + r = ref[len(tag_prefix):] + if verbose: + print("picking %s" % r) + return {"version": r, + "full-revisionid": keywords["full"].strip(), + "dirty": False, "error": None, + "date": date} + # no suitable tags, so version is "0+unknown", but full hex is still there + if verbose: + print("no suitable tags, using unknown + full revision id") + return {"version": "0+unknown", + "full-revisionid": keywords["full"].strip(), + "dirty": False, "error": "no suitable tags", "date": None} + + +@register_vcs_handler("git", "pieces_from_vcs") +def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): + """Get version from 'git describe' in the root of the source tree. + + This only gets called if the git-archive 'subst' keywords were *not* + expanded, and _version.py hasn't already been rewritten with a short + version string, meaning we're inside a checked out source tree. + """ + GITS = ["git"] + if sys.platform == "win32": + GITS = ["git.cmd", "git.exe"] + + out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, + hide_stderr=True) + if rc != 0: + if verbose: + print("Directory %s not under git control" % root) + raise NotThisMethod("'git rev-parse --git-dir' returned error") + + # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] + # if there isn't one, this yields HEX[-dirty] (no NUM) + describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty", + "--always", "--long", + "--match", "%s*" % tag_prefix], + cwd=root) + # --long was added in git-1.5.5 + if describe_out is None: + raise NotThisMethod("'git describe' failed") + describe_out = describe_out.strip() + full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) + if full_out is None: + raise NotThisMethod("'git rev-parse' failed") + full_out = full_out.strip() + + pieces = {} + pieces["long"] = full_out + pieces["short"] = full_out[:7] # maybe improved later + pieces["error"] = None + + # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] + # TAG might have hyphens. + git_describe = describe_out + + # look for -dirty suffix + dirty = git_describe.endswith("-dirty") + pieces["dirty"] = dirty + if dirty: + git_describe = git_describe[:git_describe.rindex("-dirty")] + + # now we have TAG-NUM-gHEX or HEX + + if "-" in git_describe: + # TAG-NUM-gHEX + mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) + if not mo: + # unparseable. Maybe git-describe is misbehaving? + pieces["error"] = ("unable to parse git-describe output: '%s'" + % describe_out) + return pieces + + # tag + full_tag = mo.group(1) + if not full_tag.startswith(tag_prefix): + if verbose: + fmt = "tag '%s' doesn't start with prefix '%s'" + print(fmt % (full_tag, tag_prefix)) + pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" + % (full_tag, tag_prefix)) + return pieces + pieces["closest-tag"] = full_tag[len(tag_prefix):] + + # distance: number of commits since tag + pieces["distance"] = int(mo.group(2)) + + # commit: short hex revision ID + pieces["short"] = mo.group(3) + + else: + # HEX: no tags + pieces["closest-tag"] = None + count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], + cwd=root) + pieces["distance"] = int(count_out) # total number of commits + + # commit date: see ISO-8601 comment in git_versions_from_keywords() + date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"], + cwd=root)[0].strip() + pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) + + return pieces + + +def do_vcs_install(manifest_in, versionfile_source, ipy): + """Git-specific installation logic for Versioneer. + + For Git, this means creating/changing .gitattributes to mark _version.py + for export-subst keyword substitution. + """ + GITS = ["git"] + if sys.platform == "win32": + GITS = ["git.cmd", "git.exe"] + files = [manifest_in, versionfile_source] + if ipy: + files.append(ipy) + try: + me = __file__ + if me.endswith(".pyc") or me.endswith(".pyo"): + me = os.path.splitext(me)[0] + ".py" + versioneer_file = os.path.relpath(me) + except NameError: + versioneer_file = "versioneer.py" + files.append(versioneer_file) + present = False + try: + f = open(".gitattributes", "r") + for line in f.readlines(): + if line.strip().startswith(versionfile_source): + if "export-subst" in line.strip().split()[1:]: + present = True + f.close() + except EnvironmentError: + pass + if not present: + f = open(".gitattributes", "a+") + f.write("%s export-subst\n" % versionfile_source) + f.close() + files.append(".gitattributes") + run_command(GITS, ["add", "--"] + files) + + +def versions_from_parentdir(parentdir_prefix, root, verbose): + """Try to determine the version from the parent directory name. + + Source tarballs conventionally unpack into a directory that includes both + the project name and a version string. We will also support searching up + two directory levels for an appropriately named parent directory + """ + rootdirs = [] + + for i in range(3): + dirname = os.path.basename(root) + if dirname.startswith(parentdir_prefix): + return {"version": dirname[len(parentdir_prefix):], + "full-revisionid": None, + "dirty": False, "error": None, "date": None} + else: + rootdirs.append(root) + root = os.path.dirname(root) # up a level + + if verbose: + print("Tried directories %s but none started with prefix %s" % + (str(rootdirs), parentdir_prefix)) + raise NotThisMethod("rootdir doesn't start with parentdir_prefix") + + +SHORT_VERSION_PY = """ +# This file was generated by 'versioneer.py' (0.18) from +# revision-control system data, or from the parent directory name of an +# unpacked source archive. Distribution tarballs contain a pre-generated copy +# of this file. + +import json + +version_json = ''' +%s +''' # END VERSION_JSON + + +def get_versions(): + return json.loads(version_json) +""" + + +def versions_from_file(filename): + """Try to determine the version from _version.py if present.""" + try: + with open(filename) as f: + contents = f.read() + except EnvironmentError: + raise NotThisMethod("unable to read _version.py") + mo = re.search(r"version_json = '''\n(.*)''' # END VERSION_JSON", + contents, re.M | re.S) + if not mo: + mo = re.search(r"version_json = '''\r\n(.*)''' # END VERSION_JSON", + contents, re.M | re.S) + if not mo: + raise NotThisMethod("no version_json in _version.py") + return json.loads(mo.group(1)) + + +def write_to_version_file(filename, versions): + """Write the given version number to the given _version.py file.""" + os.unlink(filename) + contents = json.dumps(versions, sort_keys=True, + indent=1, separators=(",", ": ")) + with open(filename, "w") as f: + f.write(SHORT_VERSION_PY % contents) + + print("set %s to '%s'" % (filename, versions["version"])) + + +def plus_or_dot(pieces): + """Return a + if we don't already have one, else return a .""" + if "+" in pieces.get("closest-tag", ""): + return "." + return "+" + + +def render_pep440(pieces): + """Build up version string, with post-release "local version identifier". + + Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you + get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty + + Exceptions: + 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += plus_or_dot(pieces) + rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + else: + # exception #1 + rendered = "0+untagged.%d.g%s" % (pieces["distance"], + pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + return rendered + + +def render_pep440_pre(pieces): + """TAG[.post.devDISTANCE] -- No -dirty. + + Exceptions: + 1: no tags. 0.post.devDISTANCE + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"]: + rendered += ".post.dev%d" % pieces["distance"] + else: + # exception #1 + rendered = "0.post.dev%d" % pieces["distance"] + return rendered + + +def render_pep440_post(pieces): + """TAG[.postDISTANCE[.dev0]+gHEX] . + + The ".dev0" means dirty. Note that .dev0 sorts backwards + (a dirty tree will appear "older" than the corresponding clean one), + but you shouldn't be releasing software with -dirty anyways. + + Exceptions: + 1: no tags. 0.postDISTANCE[.dev0] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += ".post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + rendered += plus_or_dot(pieces) + rendered += "g%s" % pieces["short"] + else: + # exception #1 + rendered = "0.post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + rendered += "+g%s" % pieces["short"] + return rendered + + +def render_pep440_old(pieces): + """TAG[.postDISTANCE[.dev0]] . + + The ".dev0" means dirty. + + Eexceptions: + 1: no tags. 0.postDISTANCE[.dev0] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += ".post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + else: + # exception #1 + rendered = "0.post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + return rendered + + +def render_git_describe(pieces): + """TAG[-DISTANCE-gHEX][-dirty]. + + Like 'git describe --tags --dirty --always'. + + Exceptions: + 1: no tags. HEX[-dirty] (note: no 'g' prefix) + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"]: + rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) + else: + # exception #1 + rendered = pieces["short"] + if pieces["dirty"]: + rendered += "-dirty" + return rendered + + +def render_git_describe_long(pieces): + """TAG-DISTANCE-gHEX[-dirty]. + + Like 'git describe --tags --dirty --always -long'. + The distance/hash is unconditional. + + Exceptions: + 1: no tags. HEX[-dirty] (note: no 'g' prefix) + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) + else: + # exception #1 + rendered = pieces["short"] + if pieces["dirty"]: + rendered += "-dirty" + return rendered + + +def render(pieces, style): + """Render the given version pieces into the requested style.""" + if pieces["error"]: + return {"version": "unknown", + "full-revisionid": pieces.get("long"), + "dirty": None, + "error": pieces["error"], + "date": None} + + if not style or style == "default": + style = "pep440" # the default + + if style == "pep440": + rendered = render_pep440(pieces) + elif style == "pep440-pre": + rendered = render_pep440_pre(pieces) + elif style == "pep440-post": + rendered = render_pep440_post(pieces) + elif style == "pep440-old": + rendered = render_pep440_old(pieces) + elif style == "git-describe": + rendered = render_git_describe(pieces) + elif style == "git-describe-long": + rendered = render_git_describe_long(pieces) + else: + raise ValueError("unknown style '%s'" % style) + + return {"version": rendered, "full-revisionid": pieces["long"], + "dirty": pieces["dirty"], "error": None, + "date": pieces.get("date")} + + +class VersioneerBadRootError(Exception): + """The project root directory is unknown or missing key files.""" + + +def get_versions(verbose=False): + """Get the project version from whatever source is available. + + Returns dict with two keys: 'version' and 'full'. + """ + if "versioneer" in sys.modules: + # see the discussion in cmdclass.py:get_cmdclass() + del sys.modules["versioneer"] + + root = get_root() + cfg = get_config_from_root(root) + + assert cfg.VCS is not None, "please set [versioneer]VCS= in setup.cfg" + handlers = HANDLERS.get(cfg.VCS) + assert handlers, "unrecognized VCS '%s'" % cfg.VCS + verbose = verbose or cfg.verbose + assert cfg.versionfile_source is not None, \ + "please set versioneer.versionfile_source" + assert cfg.tag_prefix is not None, "please set versioneer.tag_prefix" + + versionfile_abs = os.path.join(root, cfg.versionfile_source) + + # extract version from first of: _version.py, VCS command (e.g. 'git + # describe'), parentdir. This is meant to work for developers using a + # source checkout, for users of a tarball created by 'setup.py sdist', + # and for users of a tarball/zipball created by 'git archive' or github's + # download-from-tag feature or the equivalent in other VCSes. + + get_keywords_f = handlers.get("get_keywords") + from_keywords_f = handlers.get("keywords") + if get_keywords_f and from_keywords_f: + try: + keywords = get_keywords_f(versionfile_abs) + ver = from_keywords_f(keywords, cfg.tag_prefix, verbose) + if verbose: + print("got version from expanded keyword %s" % ver) + return ver + except NotThisMethod: + pass + + try: + ver = versions_from_file(versionfile_abs) + if verbose: + print("got version from file %s %s" % (versionfile_abs, ver)) + return ver + except NotThisMethod: + pass + + from_vcs_f = handlers.get("pieces_from_vcs") + if from_vcs_f: + try: + pieces = from_vcs_f(cfg.tag_prefix, root, verbose) + ver = render(pieces, cfg.style) + if verbose: + print("got version from VCS %s" % ver) + return ver + except NotThisMethod: + pass + + try: + if cfg.parentdir_prefix: + ver = versions_from_parentdir(cfg.parentdir_prefix, root, verbose) + if verbose: + print("got version from parentdir %s" % ver) + return ver + except NotThisMethod: + pass + + if verbose: + print("unable to compute version") + + return {"version": "0+unknown", "full-revisionid": None, + "dirty": None, "error": "unable to compute version", + "date": None} + + +def get_version(): + """Get the short version string for this project.""" + return get_versions()["version"] + + +def get_cmdclass(): + """Get the custom setuptools/distutils subclasses used by Versioneer.""" + if "versioneer" in sys.modules: + del sys.modules["versioneer"] + # this fixes the "python setup.py develop" case (also 'install' and + # 'easy_install .'), in which subdependencies of the main project are + # built (using setup.py bdist_egg) in the same python process. Assume + # a main project A and a dependency B, which use different versions + # of Versioneer. A's setup.py imports A's Versioneer, leaving it in + # sys.modules by the time B's setup.py is executed, causing B to run + # with the wrong versioneer. Setuptools wraps the sub-dep builds in a + # sandbox that restores sys.modules to it's pre-build state, so the + # parent is protected against the child's "import versioneer". By + # removing ourselves from sys.modules here, before the child build + # happens, we protect the child from the parent's versioneer too. + # Also see https://github.com/warner/python-versioneer/issues/52 + + cmds = {} + + # we add "version" to both distutils and setuptools + from distutils.core import Command + + class cmd_version(Command): + description = "report generated version string" + user_options = [] + boolean_options = [] + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + def run(self): + vers = get_versions(verbose=True) + print("Version: %s" % vers["version"]) + print(" full-revisionid: %s" % vers.get("full-revisionid")) + print(" dirty: %s" % vers.get("dirty")) + print(" date: %s" % vers.get("date")) + if vers["error"]: + print(" error: %s" % vers["error"]) + cmds["version"] = cmd_version + + # we override "build_py" in both distutils and setuptools + # + # most invocation pathways end up running build_py: + # distutils/build -> build_py + # distutils/install -> distutils/build ->.. + # setuptools/bdist_wheel -> distutils/install ->.. + # setuptools/bdist_egg -> distutils/install_lib -> build_py + # setuptools/install -> bdist_egg ->.. + # setuptools/develop -> ? + # pip install: + # copies source tree to a tempdir before running egg_info/etc + # if .git isn't copied too, 'git describe' will fail + # then does setup.py bdist_wheel, or sometimes setup.py install + # setup.py egg_info -> ? + + # we override different "build_py" commands for both environments + if "setuptools" in sys.modules: + from setuptools.command.build_py import build_py as _build_py + else: + from distutils.command.build_py import build_py as _build_py + + class cmd_build_py(_build_py): + def run(self): + root = get_root() + cfg = get_config_from_root(root) + versions = get_versions() + _build_py.run(self) + # now locate _version.py in the new build/ directory and replace + # it with an updated value + if cfg.versionfile_build: + target_versionfile = os.path.join(self.build_lib, + cfg.versionfile_build) + print("UPDATING %s" % target_versionfile) + write_to_version_file(target_versionfile, versions) + cmds["build_py"] = cmd_build_py + + if "cx_Freeze" in sys.modules: # cx_freeze enabled? + from cx_Freeze.dist import build_exe as _build_exe + # nczeczulin reports that py2exe won't like the pep440-style string + # as FILEVERSION, but it can be used for PRODUCTVERSION, e.g. + # setup(console=[{ + # "version": versioneer.get_version().split("+", 1)[0], # FILEVERSION + # "product_version": versioneer.get_version(), + # ... + + class cmd_build_exe(_build_exe): + def run(self): + root = get_root() + cfg = get_config_from_root(root) + versions = get_versions() + target_versionfile = cfg.versionfile_source + print("UPDATING %s" % target_versionfile) + write_to_version_file(target_versionfile, versions) + + _build_exe.run(self) + os.unlink(target_versionfile) + with open(cfg.versionfile_source, "w") as f: + LONG = LONG_VERSION_PY[cfg.VCS] + f.write(LONG % + {"DOLLAR": "$", + "STYLE": cfg.style, + "TAG_PREFIX": cfg.tag_prefix, + "PARENTDIR_PREFIX": cfg.parentdir_prefix, + "VERSIONFILE_SOURCE": cfg.versionfile_source, + }) + cmds["build_exe"] = cmd_build_exe + del cmds["build_py"] + + if 'py2exe' in sys.modules: # py2exe enabled? + try: + from py2exe.distutils_buildexe import py2exe as _py2exe # py3 + except ImportError: + from py2exe.build_exe import py2exe as _py2exe # py2 + + class cmd_py2exe(_py2exe): + def run(self): + root = get_root() + cfg = get_config_from_root(root) + versions = get_versions() + target_versionfile = cfg.versionfile_source + print("UPDATING %s" % target_versionfile) + write_to_version_file(target_versionfile, versions) + + _py2exe.run(self) + os.unlink(target_versionfile) + with open(cfg.versionfile_source, "w") as f: + LONG = LONG_VERSION_PY[cfg.VCS] + f.write(LONG % + {"DOLLAR": "$", + "STYLE": cfg.style, + "TAG_PREFIX": cfg.tag_prefix, + "PARENTDIR_PREFIX": cfg.parentdir_prefix, + "VERSIONFILE_SOURCE": cfg.versionfile_source, + }) + cmds["py2exe"] = cmd_py2exe + + # we override different "sdist" commands for both environments + if "setuptools" in sys.modules: + from setuptools.command.sdist import sdist as _sdist + else: + from distutils.command.sdist import sdist as _sdist + + class cmd_sdist(_sdist): + def run(self): + versions = get_versions() + self._versioneer_generated_versions = versions + # unless we update this, the command will keep using the old + # version + self.distribution.metadata.version = versions["version"] + return _sdist.run(self) + + def make_release_tree(self, base_dir, files): + root = get_root() + cfg = get_config_from_root(root) + _sdist.make_release_tree(self, base_dir, files) + # now locate _version.py in the new base_dir directory + # (remembering that it may be a hardlink) and replace it with an + # updated value + target_versionfile = os.path.join(base_dir, cfg.versionfile_source) + print("UPDATING %s" % target_versionfile) + write_to_version_file(target_versionfile, + self._versioneer_generated_versions) + cmds["sdist"] = cmd_sdist + + return cmds + + +CONFIG_ERROR = """ +setup.cfg is missing the necessary Versioneer configuration. You need +a section like: + + [versioneer] + VCS = git + style = pep440 + versionfile_source = src/myproject/_version.py + versionfile_build = myproject/_version.py + tag_prefix = + parentdir_prefix = myproject- + +You will also need to edit your setup.py to use the results: + + import versioneer + setup(version=versioneer.get_version(), + cmdclass=versioneer.get_cmdclass(), ...) + +Please read the docstring in ./versioneer.py for configuration instructions, +edit setup.cfg, and re-run the installer or 'python versioneer.py setup'. +""" + +SAMPLE_CONFIG = """ +# See the docstring in versioneer.py for instructions. Note that you must +# re-run 'versioneer.py setup' after changing this section, and commit the +# resulting files. + +[versioneer] +#VCS = git +#style = pep440 +#versionfile_source = +#versionfile_build = +#tag_prefix = +#parentdir_prefix = + +""" + +INIT_PY_SNIPPET = """ +from ._version import get_versions +__version__ = get_versions()['version'] +del get_versions +""" + + +def do_setup(): + """Main VCS-independent setup function for installing Versioneer.""" + root = get_root() + try: + cfg = get_config_from_root(root) + except (EnvironmentError, configparser.NoSectionError, + configparser.NoOptionError) as e: + if isinstance(e, (EnvironmentError, configparser.NoSectionError)): + print("Adding sample versioneer config to setup.cfg", + file=sys.stderr) + with open(os.path.join(root, "setup.cfg"), "a") as f: + f.write(SAMPLE_CONFIG) + print(CONFIG_ERROR, file=sys.stderr) + return 1 + + print(" creating %s" % cfg.versionfile_source) + with open(cfg.versionfile_source, "w") as f: + LONG = LONG_VERSION_PY[cfg.VCS] + f.write(LONG % {"DOLLAR": "$", + "STYLE": cfg.style, + "TAG_PREFIX": cfg.tag_prefix, + "PARENTDIR_PREFIX": cfg.parentdir_prefix, + "VERSIONFILE_SOURCE": cfg.versionfile_source, + }) + + ipy = os.path.join(os.path.dirname(cfg.versionfile_source), + "__init__.py") + if os.path.exists(ipy): + try: + with open(ipy, "r") as f: + old = f.read() + except EnvironmentError: + old = "" + if INIT_PY_SNIPPET not in old: + print(" appending to %s" % ipy) + with open(ipy, "a") as f: + f.write(INIT_PY_SNIPPET) + else: + print(" %s unmodified" % ipy) + else: + print(" %s doesn't exist, ok" % ipy) + ipy = None + + # Make sure both the top-level "versioneer.py" and versionfile_source + # (PKG/_version.py, used by runtime code) are in MANIFEST.in, so + # they'll be copied into source distributions. Pip won't be able to + # install the package without this. + manifest_in = os.path.join(root, "MANIFEST.in") + simple_includes = set() + try: + with open(manifest_in, "r") as f: + for line in f: + if line.startswith("include "): + for include in line.split()[1:]: + simple_includes.add(include) + except EnvironmentError: + pass + # That doesn't cover everything MANIFEST.in can do + # (http://docs.python.org/2/distutils/sourcedist.html#commands), so + # it might give some false negatives. Appending redundant 'include' + # lines is safe, though. + if "versioneer.py" not in simple_includes: + print(" appending 'versioneer.py' to MANIFEST.in") + with open(manifest_in, "a") as f: + f.write("include versioneer.py\n") + else: + print(" 'versioneer.py' already in MANIFEST.in") + if cfg.versionfile_source not in simple_includes: + print(" appending versionfile_source ('%s') to MANIFEST.in" % + cfg.versionfile_source) + with open(manifest_in, "a") as f: + f.write("include %s\n" % cfg.versionfile_source) + else: + print(" versionfile_source already in MANIFEST.in") + + # Make VCS-specific changes. For git, this means creating/changing + # .gitattributes to mark _version.py for export-subst keyword + # substitution. + do_vcs_install(manifest_in, cfg.versionfile_source, ipy) + return 0 + + +def scan_setup_py(): + """Validate the contents of setup.py against Versioneer's expectations.""" + found = set() + setters = False + errors = 0 + with open("setup.py", "r") as f: + for line in f.readlines(): + if "import versioneer" in line: + found.add("import") + if "versioneer.get_cmdclass()" in line: + found.add("cmdclass") + if "versioneer.get_version()" in line: + found.add("get_version") + if "versioneer.VCS" in line: + setters = True + if "versioneer.versionfile_source" in line: + setters = True + if len(found) != 3: + print("") + print("Your setup.py appears to be missing some important items") + print("(but I might be wrong). Please make sure it has something") + print("roughly like the following:") + print("") + print(" import versioneer") + print(" setup( version=versioneer.get_version(),") + print(" cmdclass=versioneer.get_cmdclass(), ...)") + print("") + errors += 1 + if setters: + print("You should remove lines like 'versioneer.VCS = ' and") + print("'versioneer.versionfile_source = ' . This configuration") + print("now lives in setup.cfg, and should be removed from setup.py") + print("") + errors += 1 + return errors + + +if __name__ == "__main__": + cmd = sys.argv[1] + if cmd == "setup": + errors = do_setup() + errors += scan_setup_py() + if errors: + sys.exit(1) -- cgit v1.2.3 From e0828b3c87b180a02bd9fa45ac908d7ed636385d Mon Sep 17 00:00:00 2001 From: Neil Williams Date: Sat, 16 Jul 2022 18:56:16 +0200 Subject: Import python-pyepics_3.4.1+ds-3.debian.tar.xz [dgit import tarball python-pyepics 3.4.1+ds-3 python-pyepics_3.4.1+ds-3.debian.tar.xz] --- README.Debian | 12 + changelog | 37 +++ control | 66 +++++ copyright | 303 +++++++++++++++++++++++ gbp.conf | 7 + patches/0002-Disable-clibs-package.patch | 24 ++ patches/0003-Improve-Python3-compatibility.patch | 21 ++ patches/fix-_find_lib.patch | 18 ++ patches/requirements.patch | 10 + patches/series | 4 + py3dist-overrides | 1 + python-pyepics-doc.doc-base | 8 + python-pyepics-doc.docs | 1 + python-pyepics-doc.lintian-overrides | 3 + python3-pyepics.lintian-overrides | 1 + rules | 21 ++ salsa-ci.yml | 11 + source/format | 1 + source/options | 1 + tests/autopkgtest-pkg-python.conf | 1 + tests/control | 3 + tests/imports | 6 + watch | 6 + 23 files changed, 566 insertions(+) create mode 100644 README.Debian create mode 100644 changelog create mode 100644 control create mode 100644 copyright create mode 100644 gbp.conf create mode 100644 patches/0002-Disable-clibs-package.patch create mode 100644 patches/0003-Improve-Python3-compatibility.patch create mode 100644 patches/fix-_find_lib.patch create mode 100644 patches/requirements.patch create mode 100644 patches/series create mode 100644 py3dist-overrides create mode 100644 python-pyepics-doc.doc-base create mode 100644 python-pyepics-doc.docs create mode 100644 python-pyepics-doc.lintian-overrides create mode 100644 python3-pyepics.lintian-overrides create mode 100755 rules create mode 100644 salsa-ci.yml create mode 100644 source/format create mode 100644 source/options create mode 100644 tests/autopkgtest-pkg-python.conf create mode 100644 tests/control create mode 100755 tests/imports create mode 100644 watch diff --git a/README.Debian b/README.Debian new file mode 100644 index 0000000..6af3d50 --- /dev/null +++ b/README.Debian @@ -0,0 +1,12 @@ +pyepics for Debian +---------------- + +The upstream INSTALL file describes importing a lib module as epics. +The Debian packaging creates the 'epics' module, so import that directly. + +Instead of: + import lib as epics +use: + import epics + + -- Neil Williams Mon, 31 Jan 2022 10:46:27 +0000 diff --git a/changelog b/changelog new file mode 100644 index 0000000..33bc6f2 --- /dev/null +++ b/changelog @@ -0,0 +1,37 @@ +python-pyepics (3.4.1+ds-3) unstable; urgency=medium + + * Remove autopkgtest-pkg-python + * Limit internal tests to amd64 only + + -- Neil Williams Sat, 16 Jul 2022 17:56:16 +0100 + +python-pyepics (3.4.1+ds-2) unstable; urgency=medium + + * Source-only upload to support migration + * Restrict to amd64 only for EPICS support + + -- Neil Williams Fri, 15 Jul 2022 21:18:29 +0100 + +python-pyepics (3.4.1+ds-1) unstable; urgency=medium + + [ Sébastien Delafond ] + * d/control: section + * Fix doc package + * Add myself to Uploaders and debian/ copyright + * Better patch description for 0002 + * Update 0002 patch name + * Add d/salsa-ci.yml + * Do not use python3 for documentation package + * Add d/gbp.conf + * Tell autodep8 test to use epics as the module name + * Depend on python3-pkg-resources + + [ Andrius Merkys ] + * Initial release (Closes: #985486) + + [ Neil Williams ] + * Bump debhelper support and standards version + * Update d.copyright + * Fix omissions in d.copyright + + -- Neil Williams Wed, 13 Jul 2022 10:39:22 +0100 diff --git a/control b/control new file mode 100644 index 0000000..d2ad401 --- /dev/null +++ b/control @@ -0,0 +1,66 @@ +Source: python-pyepics +Section: python +Priority: optional +Maintainer: Debian PaN Maintainers +Uploaders: + Debian Science Maintainers , + Andrius Merkys , + Sebastien Delafond , + Picca Frédéric-Emmanuel , + Neil Williams , +Build-Depends: + debhelper-compat (= 13), + libca-dev, + libcom-dev, + dh-python, + python3-all:any, + python3-numpy, + python3-numpydoc , + python3-setuptools, + python3-six, + python3-sphinx , + python3-wxgtk4.0, + python3-pyparsing +Standards-Version: 4.6.0 +Homepage: https://pyepics.github.io/pyepics/ +Vcs-Browser: https://salsa.debian.org/science-team/python-pyepics +Vcs-Git: https://salsa.debian.org/science-team/python-pyepics.git + +Package: python3-pyepics +Architecture: amd64 +Depends: + ${python3:Depends}, + ${misc:Depends}, + python3-pkg-resources, + libca4.13.5, + libcom3.17.6 +Suggests: python-pyepics-doc +Description: EPICS channel access for Python + PyEpics3 is a Python interface to the EPICS Channel Access (CA) library + for the Experimental Physics and Industrial Control System (EPICS). + . + The PyEpics module includes both low-level (C-like) and higher-level access + (with Python objects) to the EPICS Channel Access (CA) protocol. Python's + ctypes library is used to wrap the basic CA functionality, with higher + level objects on top of that basic interface. This approach has several + advantages including no need for extension code written in C, better + thread-safety, and easier installation on multiple platforms. + . + This package installs the library for Python 3. + +Package: python-pyepics-doc +Architecture: all +Section: doc +Depends: ${sphinxdoc:Depends}, ${misc:Depends} +Description: EPICS channel access for Python (common documentation) + PyEpics is a Python interface to the EPICS Channel Access (CA) library + for the Experimental Physics and Industrial Control System (EPICS). + . + The PyEpics module includes both low-level (C-like) and higher-level access + (with Python objects) to the EPICS Channel Access (CA) protocol. Python's + ctypes library is used to wrap the basic CA functionality, with higher + level objects on top of that basic interface. This approach has several + advantages including no need for extension code written in C, better + thread-safety, and easier installation on multiple platforms. + . + This is the PyEpics3 documentation package. diff --git a/copyright b/copyright new file mode 100644 index 0000000..f7a6988 --- /dev/null +++ b/copyright @@ -0,0 +1,303 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: python-pyepics +Upstream-Contact: Matthew Newville +Source: https://pyepics.github.io/pyepics/ +Files-Excluded: + pyepics.egg-info + __pycache__ + epics/clibs + epics/__pycache__ + +Files: * +Copyright: 2010 Matthew Newville , + CARS, University of Chicago, + Angus Gratton +License: EPICS + +Files: doc/sphinx/theme/epicsdoc/* +Copyright: 2007-2011 by the Sphinx team +License: BSD-2-clause + +Files: epics/motor.py +Copyright: 2002 Mark Rivers / Matt Newville +License: EPICS + +Files: epics/multiproc.py +Copyright: 2014 Ken Lauer +License: EPICS + +Files: versioneer.py +Copyright: 2017 Brian Warner +Comment: https://github.com/python-versioneer/python-versioneer/blob/master/LICENSE +License: CC0 + +Files: epics/devices/ordereddict.py epics/wx/ordereddict.py +Copyright: 2009 Raymond Hettinger +License: Expat + +Files: debian/* +Copyright: 2020-2021, Andrius Merkys + 2021 Sebastien Delafond + 2022 Neil Williams +License: GPL-2+ + This package is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + . + This package is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + . + You should have received a copy of the GNU General Public License + along with this program. If not, see + . + On Debian systems, the complete text of the GNU General + Public License version 2 can be found in "/usr/share/common-licenses/GPL-2". + +License: CC0 + Creative Commons Legal Code + . + CC0 1.0 Universal + . + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + . + Statement of Purpose + . + The laws of most jurisdictions throughout the world automatically confer + exclusive Copyright and Related Rights (defined below) upon the creator + and subsequent owner(s) (each and all, an "owner") of an original work of + authorship and/or a database (each, a "Work"). + . + Certain owners wish to permanently relinquish those rights to a Work for + the purpose of contributing to a commons of creative, cultural and + scientific works ("Commons") that the public can reliably and without fear + of later claims of infringement build upon, modify, incorporate in other + works, reuse and redistribute as freely as possible in any form whatsoever + and for any purposes, including without limitation commercial purposes. + These owners may contribute to the Commons to promote the ideal of a free + culture and the further production of creative, cultural and scientific + works, or to gain reputation or greater distribution for their Work in + part through the use and efforts of others. + . + For these and/or other purposes and motivations, and without any + expectation of additional consideration or compensation, the person + associating CC0 with a Work (the "Affirmer"), to the extent that he or she + is an owner of Copyright and Related Rights in the Work, voluntarily + elects to apply CC0 to the Work and publicly distribute the Work under its + terms, with knowledge of his or her Copyright and Related Rights in the + Work and the meaning and intended legal effect of CC0 on those rights. + . + 1. Copyright and Related Rights. A Work made available under CC0 may be + protected by copyright and related or neighboring rights ("Copyright and + Related Rights"). Copyright and Related Rights include, but are not + limited to, the following: + . + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); + iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and + vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + . + 2. Waiver. To the greatest extent permitted by, but not in contravention + of, applicable law, Affirmer hereby overtly, fully, permanently, + irrevocably and unconditionally waives, abandons, and surrenders all of + Affirmer's Copyright and Related Rights and associated claims and causes + of action, whether now known or unknown (including existing as well as + future claims and causes of action), in the Work (i) in all territories + worldwide, (ii) for the maximum duration provided by applicable law or + treaty (including future time extensions), (iii) in any current or future + medium and for any number of copies, and (iv) for any purpose whatsoever, + including without limitation commercial, advertising or promotional + purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each + member of the public at large and to the detriment of Affirmer's heirs and + successors, fully intending that such Waiver shall not be subject to + revocation, rescission, cancellation, termination, or any other legal or + equitable action to disrupt the quiet enjoyment of the Work by the public + as contemplated by Affirmer's express Statement of Purpose. + . + 3. Public License Fallback. Should any part of the Waiver for any reason + be judged legally invalid or ineffective under applicable law, then the + Waiver shall be preserved to the maximum extent permitted taking into + account Affirmer's express Statement of Purpose. In addition, to the + extent the Waiver is so judged Affirmer hereby grants to each affected + person a royalty-free, non transferable, non sublicensable, non exclusive, + irrevocable and unconditional license to exercise Affirmer's Copyright and + Related Rights in the Work (i) in all territories worldwide, (ii) for the + maximum duration provided by applicable law or treaty (including future + time extensions), (iii) in any current or future medium and for any number + of copies, and (iv) for any purpose whatsoever, including without + limitation commercial, advertising or promotional purposes (the + "License"). The License shall be deemed effective as of the date CC0 was + applied by Affirmer to the Work. Should any part of the License for any + reason be judged legally invalid or ineffective under applicable law, such + partial invalidity or ineffectiveness shall not invalidate the remainder + of the License, and in such case Affirmer hereby affirms that he or she + will not (i) exercise any of his or her remaining Copyright and Related + Rights in the Work or (ii) assert any associated claims and causes of + action with respect to the Work, in either case contrary to Affirmer's + express Statement of Purpose. + . + 4. Limitations and Disclaimers. + . + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. + +License: Expat + By obtaining, using, and/or copying this software and/or its + associated documentation, you agree that you have read, understood, + and will comply with the following terms and conditions: + . + Permission to use, copy, modify, and distribute this software and + its associated documentation for any purpose and without fee is + hereby granted, provided that the above copyright notice appears in + all copies, and that both that copyright notice and this permission + notice appear in supporting documentation, and that the name of + Secret Labs AB or the author not be used in advertising or publicity + pertaining to distribution of the software without specific, written + prior permission. + . + SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD + TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT- + ABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR + BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY + DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, + WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS + ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE + OF THIS SOFTWARE. + +License: BSD-2-clause + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + . + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + . + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + +License: EPICS + SOFTWARE LICENSE AGREEMENT + Software: EPICS BASE + . + 1. The "Software", below, refers to EPICS BASE (in either source code, or + binary form and accompanying documentation). Each licensee is + addressed as "you" or "Licensee." + . + 2. The copyright holders shown above and their third-party licensors + hereby grant Licensee a royalty-free nonexclusive license, subject to + the limitations stated herein and U.S. Government license rights. + . + 3. You may modify and make a copy or copies of the Software for use + within your organization, if you meet the following conditions: + a. Copies in source code must include the copyright notice and this + Software License Agreement. + b. Copies in binary form must include the copyright notice and this + Software License Agreement in the documentation and/or other + materials provided with the copy. + . + 4. You may modify a copy or copies of the Software or any portion of it, + thus forming a work based on the Software, and distribute copies of + such work outside your organization, if you meet all of the following + conditions: + a. Copies in source code must include the copyright notice and this + Software License Agreement; + b. Copies in binary form must include the copyright notice and this + Software License Agreement in the documentation and/or other + materials provided with the copy; + c. Modified copies and works based on the Software must carry + prominent notices stating that you changed specified portions of + the Software. + . + 5. Portions of the Software resulted from work developed under a U.S. + Government contract and are subject to the following license: the + Government is granted for itself and others acting on its behalf a + paid-up, nonexclusive, irrevocable worldwide license in this computer + software to reproduce, prepare derivative works, and perform publicly + and display publicly. + . + 6. WARRANTY DISCLAIMER. THE SOFTWARE IS SUPPLIED "AS IS" WITHOUT WARRANTY + OF ANY KIND. THE COPYRIGHT HOLDERS, THEIR THIRD PARTY LICENSORS, THE + UNITED STATES, THE UNITED STATES DEPARTMENT OF ENERGY, AND THEIR + EMPLOYEES: (1) DISCLAIM ANY WARRANTIES, EXPRESS OR IMPLIED, INCLUDING + BUT NOT LIMITED TO ANY IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS + FOR A PARTICULAR PURPOSE, TITLE OR NON-INFRINGEMENT, (2) DO NOT ASSUME + ANY LEGAL LIABILITY OR RESPONSIBILITY FOR THE ACCURACY, COMPLETENESS, + OR USEFULNESS OF THE SOFTWARE, (3) DO NOT REPRESENT THAT USE OF THE + SOFTWARE WOULD NOT INFRINGE PRIVATELY OWNED RIGHTS, (4) DO NOT WARRANT + THAT THE SOFTWARE WILL FUNCTION UNINTERRUPTED, THAT IT IS ERROR-FREE + OR THAT ANY ERRORS WILL BE CORRECTED. + . + 7. LIMITATION OF LIABILITY. IN NO EVENT WILL THE COPYRIGHT HOLDERS, THEIR + THIRD PARTY LICENSORS, THE UNITED STATES, THE UNITED STATES DEPARTMENT + OF ENERGY, OR THEIR EMPLOYEES: BE LIABLE FOR ANY INDIRECT, INCIDENTAL, + CONSEQUENTIAL, SPECIAL OR PUNITIVE DAMAGES OF ANY KIND OR NATURE, + INCLUDING BUT NOT LIMITED TO LOSS OF PROFITS OR LOSS OF DATA, FOR ANY + REASON WHATSOEVER, WHETHER SUCH LIABILITY IS ASSERTED ON THE BASIS OF + CONTRACT, TORT (INCLUDING NEGLIGENCE OR STRICT LIABILITY), OR + OTHERWISE, EVEN IF ANY OF SAID PARTIES HAS BEEN WARNED OF THE + POSSIBILITY OF SUCH LOSS OR DAMAGES. + . + ________________________________________________________________________ + . + This software is in part copyrighted by the BERLINER SPEICHERRING + GESELLSCHAFT FUER SYNCHROTRONSTRAHLUNG M.B.H. (BESSY), BERLIN, GERMANY. + . + In no event shall BESSY be liable to any party for direct, indirect, + special, incidental, or consequential damages arising out of the use of + this software, its documentation, or any derivatives thereof, even if + BESSY has been advised of the possibility of such damage. + . + BESSY specifically disclaims any warranties, including, but not limited + to, the implied warranties of merchantability, fitness for a particular + purpose, and non-infringement. This software is provided on an "as is" + basis, and BESSY has no obligation to provide maintenance, support, + updates, enhancements, or modifications. + ________________________________________________________________________ diff --git a/gbp.conf b/gbp.conf new file mode 100644 index 0000000..7a7a3ba --- /dev/null +++ b/gbp.conf @@ -0,0 +1,7 @@ +[DEFAULT] +debian-branch = master +upstream-branch = upstream +pristine-tar = True + +[buildpackage] +compression = xz diff --git a/patches/0002-Disable-clibs-package.patch b/patches/0002-Disable-clibs-package.patch new file mode 100644 index 0000000..30f7eaf --- /dev/null +++ b/patches/0002-Disable-clibs-package.patch @@ -0,0 +1,24 @@ +From: Sebastien Delafond +Date: Mon, 10 May 2021 17:55:53 +0200 +Subject: Disable clibs package + +--- + setup.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + mode change 100644 => 100755 setup.py + +diff --git a/setup.py b/setup.py +old mode 100644 +new mode 100755 +index 7ca751f..443892b +--- a/setup.py ++++ b/setup.py +@@ -73,7 +73,7 @@ + 'Programming Language :: Python', + 'Topic :: Scientific/Engineering'], + packages = ['epics','epics.wx','epics.devices', 'epics.compat', +- 'epics.autosave', 'epics.clibs'], ++ 'epics.autosave'], + package_data = pkg_data, + ) + diff --git a/patches/0003-Improve-Python3-compatibility.patch b/patches/0003-Improve-Python3-compatibility.patch new file mode 100644 index 0000000..4379a2b --- /dev/null +++ b/patches/0003-Improve-Python3-compatibility.patch @@ -0,0 +1,21 @@ +From: Roland Mas +Date: Tue, 28 Sep 2021 19:35:05 +0200 +Subject: Improve Python3 compatibility + +--- + doc/conf.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/doc/conf.py b/doc/conf.py +index e50affe..5ea3c2f 100644 +--- a/doc/conf.py ++++ b/doc/conf.py +@@ -59,7 +59,7 @@ try: + except ImportError: + release = '3.X.Y' + +-print 'Building Docs for EPICS version %s / Python version %s ' % (release, sys.version) ++print ('Building Docs for EPICS version %s / Python version %s ' % (release, sys.version)) + + # The language for content autogenerated by Sphinx. Refer to documentation + # for a list of supported languages. diff --git a/patches/fix-_find_lib.patch b/patches/fix-_find_lib.patch new file mode 100644 index 0000000..020590c --- /dev/null +++ b/patches/fix-_find_lib.patch @@ -0,0 +1,18 @@ +Description: Fixing location of shared libraries. +Author: Andrius Merkys +--- a/epics/ca.py ++++ b/epics/ca.py +@@ -277,10 +277,10 @@ + return dllpath + + # Test 2: look in installed python location for dll +- dllpath = resource_filename('epics.clibs', clib_search_path(inp_lib_name)) ++ ## dllpath = resource_filename('epics.clibs', clib_search_path(inp_lib_name)) + +- if (os.path.exists(dllpath) and os.path.isfile(dllpath)): +- return dllpath ++ ## if (os.path.exists(dllpath) and os.path.isfile(dllpath)): ++ ## return dllpath + + # Test 3: look through Python path and PATH env var for dll + path_sep = ':' diff --git a/patches/requirements.patch b/patches/requirements.patch new file mode 100644 index 0000000..c9807b5 --- /dev/null +++ b/patches/requirements.patch @@ -0,0 +1,10 @@ +--- a/setup.py ++++ b/setup.py +@@ -67,6 +67,7 @@ + license = 'Epics Open License', + description = "Epics Channel Access for Python", + long_description = long_desc, ++ install_requires = ['numpy', 'wx'], + platforms = ['Windows', 'Linux', 'Mac OS X'], + classifiers = ['Intended Audience :: Science/Research', + 'Operating System :: OS Independent', diff --git a/patches/series b/patches/series new file mode 100644 index 0000000..c92cf78 --- /dev/null +++ b/patches/series @@ -0,0 +1,4 @@ +fix-_find_lib.patch +0002-Disable-clibs-package.patch +0003-Improve-Python3-compatibility.patch +requirements.patch diff --git a/py3dist-overrides b/py3dist-overrides new file mode 100644 index 0000000..8c7d448 --- /dev/null +++ b/py3dist-overrides @@ -0,0 +1 @@ +wx python3-wxgtk4.0 diff --git a/python-pyepics-doc.doc-base b/python-pyepics-doc.doc-base new file mode 100644 index 0000000..5ab0da9 --- /dev/null +++ b/python-pyepics-doc.doc-base @@ -0,0 +1,8 @@ +Document: python3-pyepics +Title: Interface to the EPICS Channel Access (CA) library +Author: Matthew Newville +Section: Programming/Python + +Format: HTML +Index: /usr/share/doc/python-pyepics-doc/html/index.html +Files: /usr/share/doc/python-pyepics-doc/html diff --git a/python-pyepics-doc.docs b/python-pyepics-doc.docs new file mode 100644 index 0000000..6d28621 --- /dev/null +++ b/python-pyepics-doc.docs @@ -0,0 +1 @@ +build/html diff --git a/python-pyepics-doc.lintian-overrides b/python-pyepics-doc.lintian-overrides new file mode 100644 index 0000000..9dd1b87 --- /dev/null +++ b/python-pyepics-doc.lintian-overrides @@ -0,0 +1,3 @@ +# Documentation, no Python2 code. +python-pyepics-doc: new-package-should-not-package-python2-module python-pyepics-doc + diff --git a/python3-pyepics.lintian-overrides b/python3-pyepics.lintian-overrides new file mode 100644 index 0000000..36535ed --- /dev/null +++ b/python3-pyepics.lintian-overrides @@ -0,0 +1 @@ +python3-pyepics: unusual-interpreter /usr/bin/ao [usr/lib/python3/dist-packages/epics/devices/ao.py] diff --git a/rules b/rules new file mode 100755 index 0000000..01d57de --- /dev/null +++ b/rules @@ -0,0 +1,21 @@ +#!/usr/bin/make -f + +export PYBUILD_NAME=pyepics +export NOLIBCA=1 + +%: + dh $@ --with python3,sphinxdoc --buildsystem=pybuild + +override_dh_auto_build: + dh_auto_build + PYTHONPATH=. http_proxy='127.0.0.1:9' \ + python3 -m sphinx -N -b html doc build/html + +override_dh_sphinxdoc: +ifeq (,$(findstring nodoc,$(DEB_BUILD_OPTIONS))) + dh_sphinxdoc -p python-pyepics-doc +endif + +override_dh_clean: + dh_clean + $(RM) -r pyepics.egg-info diff --git a/salsa-ci.yml b/salsa-ci.yml new file mode 100644 index 0000000..113a5cb --- /dev/null +++ b/salsa-ci.yml @@ -0,0 +1,11 @@ +--- +include: + - https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/salsa-ci.yml + - https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/pipeline-jobs.yml + +variables: + RELEASE: 'unstable' + SALSA_CI_DISABLE_BUILD_PACKAGE_I386: 1 + +autopkgtest: + allow_failure: true diff --git a/source/format b/source/format new file mode 100644 index 0000000..163aaf8 --- /dev/null +++ b/source/format @@ -0,0 +1 @@ +3.0 (quilt) diff --git a/source/options b/source/options new file mode 100644 index 0000000..cb61fa5 --- /dev/null +++ b/source/options @@ -0,0 +1 @@ +extend-diff-ignore = "^[^/]*[.]egg-info/" diff --git a/tests/autopkgtest-pkg-python.conf b/tests/autopkgtest-pkg-python.conf new file mode 100644 index 0000000..dab76f0 --- /dev/null +++ b/tests/autopkgtest-pkg-python.conf @@ -0,0 +1 @@ +import_name = epics diff --git a/tests/control b/tests/control new file mode 100644 index 0000000..b62497c --- /dev/null +++ b/tests/control @@ -0,0 +1,3 @@ +Tests: imports +Architecture: amd64 +Depends: @ diff --git a/tests/imports b/tests/imports new file mode 100755 index 0000000..fa8e4ff --- /dev/null +++ b/tests/imports @@ -0,0 +1,6 @@ +#!/usr/bin/python3 + +import epics + +epics.ca.find_libca() + diff --git a/watch b/watch new file mode 100644 index 0000000..198c524 --- /dev/null +++ b/watch @@ -0,0 +1,6 @@ +version=4 + +# PyPI +opts="repack,repacksuffix=+ds,dversionmangle=s/\+ds$//" \ +https://pypi.debian.net/pyepics \ + /pyepics/pyepics-(.+)\.(?:zip|tgz|tbz|txz|(?:tar\.(?:gz|bz2|xz))) -- cgit v1.2.3 From 24b242dc780a89073bf46cbb5f67df7d546b0fb4 Mon Sep 17 00:00:00 2001 From: Andrius Merkys Date: Sat, 16 Jul 2022 18:56:16 +0200 Subject: Fixing location of shared libraries. Gbp-Pq: Name fix-_find_lib.patch --- epics/ca.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/epics/ca.py b/epics/ca.py index 5c464ed..dec7c82 100755 --- a/epics/ca.py +++ b/epics/ca.py @@ -277,10 +277,10 @@ def _find_lib(inp_lib_name): return dllpath # Test 2: look in installed python location for dll - dllpath = resource_filename('epics.clibs', clib_search_path(inp_lib_name)) + ## dllpath = resource_filename('epics.clibs', clib_search_path(inp_lib_name)) - if (os.path.exists(dllpath) and os.path.isfile(dllpath)): - return dllpath + ## if (os.path.exists(dllpath) and os.path.isfile(dllpath)): + ## return dllpath # Test 3: look through Python path and PATH env var for dll path_sep = ':' -- cgit v1.2.3 From b000677b596f53355ab9211b407f80cd9daeaec3 Mon Sep 17 00:00:00 2001 From: Sebastien Delafond Date: Mon, 10 May 2021 17:55:53 +0200 Subject: Disable clibs package Gbp-Pq: Name 0002-Disable-clibs-package.patch --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) mode change 100644 => 100755 setup.py diff --git a/setup.py b/setup.py old mode 100644 new mode 100755 index 7ca751f..443892b --- a/setup.py +++ b/setup.py @@ -73,7 +73,7 @@ setup(name = 'pyepics', 'Programming Language :: Python', 'Topic :: Scientific/Engineering'], packages = ['epics','epics.wx','epics.devices', 'epics.compat', - 'epics.autosave', 'epics.clibs'], + 'epics.autosave'], package_data = pkg_data, ) -- cgit v1.2.3 From 61a2952e3e270ec19a8d5821e0f7e19298e72e7e Mon Sep 17 00:00:00 2001 From: Roland Mas Date: Tue, 28 Sep 2021 19:35:05 +0200 Subject: Improve Python3 compatibility Gbp-Pq: Name 0003-Improve-Python3-compatibility.patch --- doc/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/conf.py b/doc/conf.py index e50affe..5ea3c2f 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -59,7 +59,7 @@ try: except ImportError: release = '3.X.Y' -print 'Building Docs for EPICS version %s / Python version %s ' % (release, sys.version) +print ('Building Docs for EPICS version %s / Python version %s ' % (release, sys.version)) # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -- cgit v1.2.3 From d5a170e10d1210ba62991e5714c671928e370d39 Mon Sep 17 00:00:00 2001 From: Debian PaN Maintainers Date: Sat, 16 Jul 2022 18:56:16 +0200 Subject: requirements Gbp-Pq: Name requirements.patch --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 443892b..9fcb4be 100755 --- a/setup.py +++ b/setup.py @@ -67,6 +67,7 @@ setup(name = 'pyepics', license = 'Epics Open License', description = "Epics Channel Access for Python", long_description = long_desc, + install_requires = ['numpy', 'wx'], platforms = ['Windows', 'Linux', 'Mac OS X'], classifiers = ['Intended Audience :: Science/Research', 'Operating System :: OS Independent', -- cgit v1.2.3