Pyepics-Compatible Client

What is This?

Pyepics is a well-established Python wrapper of libca. Caproto includes a client that is a drop-in replacement for pyepics. It is implemented as a shim on top of caproto’s main Threading Client. Caproto’s pyepics-compatible client is tested against a representative sample of the pyepics test suite.

Why would you ever want to use caproto’s pyepics instead of actual pyepics? It may be advantageous to run existing user code written for pyepics on top of caproto — for example, to leverage caproto’s verbose logging or portability.

Why are there two threading clients in caproto instead of just one pyepics-compatible one? Caproto’s main threading client makes different design choices, consistent with the rest of caproto:

  1. Caproto’s threading client provides a lower-level API, handing the user objects encapsulating the complete response from the server as opposed to just the value.
  2. Caproto pulls apart the subscription process into two steps—specifying a subscription and adding a user callback function to one—whereas pyepics elides them.

Caproto is speed-competitive with pyepics. Because it controls the entire network stack, rather than calling out to libca, it can batch requests into UDP datagrams and TCP packets more efficiently, leading to a ~250X speedup in connecting a large number of channels in bulk.

The authors of caproto are heavy pyepics users and occasional contributors. This module is intended as a friendly bridge to pyepics.

Demonstration

For full documentation on pyepics usage, see the pyepics doucmentation. This is a brief demonstration of caproto’s pyepics-compat client.

In a separate shell, start one of caproto’s demo IOCs.

$ python3 -m caproto.ioc_examples.random_walk
PVs: ['random_walk:dt', 'random_walk:x']

Now, in Python we will talk to it using caproto’s pyepics-compatible client. Get and put to random_walk:dt:

In [1]: import caproto.threading.pyepics_compat as epics

In [2]: pv_name = 'random_walk:dt'

In [3]: epics.caget(pv_name)
Out[3]: 3.0

In [4]: epics.caput(pv_name, 2)

In [5]: pv = epics.get_pv(pv_name)

In [6]: pv.get()
Out[6]: 2.0

In [7]: pv.put(1)

In [8]: pv.get()
Out[8]: 1.0

Subscribe a user-defined callback function to random_walk:x:

In [9]: def f(value, **kwargs):
   ...:     print('received value' , value)
   ...: 

Note that pyepics recommends using epics.get_pv(...) instead of epics.PV(...) and so do we, but both usages are supported.

In [10]: x_pv = epics.PV('random_walk:x')

In [11]: x_pv.add_callback(f)
Out[11]: 0

In [12]: import time; time.sleep(5)  # give some time for responses to come in
received value -0.29272035252215933
received value -0.21033276624140873
received value 0.125439005418035
received value -0.5735384157266383

In [13]: x_pv.clear_callbacks()

The underlying caproto PV and Context objects from caproto’s main Threading Client. are accessible:

In [14]: pv._caproto_pv
Out[14]: <PV name='random_walk:dt' priority=0 address=('127.0.0.1', 49708), circuit_state=States.CONNECTED, channel_state=States.CONNECTED>

In [15]: pv._caproto_pv.context
Out[15]: <Context searches_pending=0 circuits=1 pvs=2 idle=0>

This brief demonstration has not exercised every aspect of the pyepics API, but caproto’s test suite is more comprehensive.