{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Connect Bluesky with EPICS\n", "\n", "From *APS Python Training for Bluesky Data Acquisition*.\n", "\n", "**Objective**:\n", "\n", "Connect Bluesky with EPICS Process Variable(s)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Fundamentals\n", "\n", "In Bluesky, the [ophyd](https://blueskyproject.io/ophyd) package is used to connect with the underlying control system (EPICS, in this case). The fundamental structure is the [EpicsSignal](https://blueskyproject.io/ophyd/tutorials/single-PV.html#connect-to-a-pv-from-ophyd) which connects a single EPICS Process Variable (PV) with a single python object. Behind the scenes, the connection is made using the [PyEpics](https://cars9.uchicago.edu/software/python/pyepics3/) package and the EPICS Channel Access protocol. Other information, when available (such as units, limits, displayed precision, data type, enumeration labels), is obtained from EPICS.\n", "\n", "ophyd class | description\n", "--- | ---\n", "`EpicsSignal` | establish a read/write connection with an EPICS PV, monitor and update the object in the background\n", "`EpicsSignalRO` | read-only version of `EpicsSignal`\n", "\n", "Note that `EpicsSignal` & `EpicsSignalRO` have many additional configuration features beyond what is described here. Consult the ophyd documentation or [source code](https://github.com/bluesky/ophyd/blob/master/ophyd/signal.py#L1346) for more details.\n", "\n", "## `EpicsSignal`\n", "\n", "Start by importing the support from *ophyd*." ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "from ophyd import EpicsSignal" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "There is an EPICS PV (`gp:gp:bit1`) with which we can connect. This PV is a single bit. Connect with it, naming the python object as `bit1` (for convenience, we make an `ioc` prefix variable, just in case someone has an EPICS IOC with a different prefix).\n", "\n", "In addition to the EPICS PV, it is required to add a `name` keyword so the Python object can be constructed with the object name. By convention, use the same name as on the left side of the `=` sign." ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "bit1.connected = False\n" ] } ], "source": [ "ioc = \"gp:\"\n", "bit1 = EpicsSignal(f\"{ioc}gp:bit1\", name=\"bit1\")\n", "print(f\"{bit1.connected = }\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Immediately after creating the connection, we tested if the Python object had fully connected with EPICS (usually this takes a short time but it is not instantaneous). As shown, the object has not connected with EPICS yet.\n", "\n", "When humans interact with a Jupyter notebook, the EPICS PV connection usually happens within the time it takes to move to the next cell and execute it. But when these PV connections are executed by a single program and the object is needed right away, it may be necessary to wait for the connection to complete before proceeding." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "bit1.connected = True\n" ] } ], "source": [ "bit1.wait_for_connection()\n", "print(f\"{bit1.connected = }\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Print the value of the Python object using its `get()` method:" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "bit1.get() = 0\n" ] } ], "source": [ "print(f\"{bit1.get() = }\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "There may be labels associated with the two different values this PV can take. Show them with the object's `enum_strs` property:" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "bit1.enum_strs = ('off', 'on')\n" ] } ], "source": [ "print(f\"{bit1.enum_strs = }\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can set this with either numbers (0 or 1) or with text (`off` or `on`) and we can show this as either numbers or text." ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "bit1.get() = 1\n", "bit1.get(as_string=False) = 1\n", "bit1.get(as_string=True) = 'on'\n", "bit1.get() = 0\n", "bit1.get(as_string=False) = 0\n", "bit1.get(as_string=True) = 'off'\n" ] } ], "source": [ "bit1.put(1)\n", "print(f\"{bit1.get() = }\")\n", "print(f\"{bit1.get(as_string=False) = }\")\n", "print(f\"{bit1.get(as_string=True) = }\")\n", "\n", "bit1.put(\"off\")\n", "print(f\"{bit1.get() = }\")\n", "print(f\"{bit1.get(as_string=False) = }\")\n", "print(f\"{bit1.get(as_string=True) = }\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can also make the text representation the default by adding the `string=True` keyword when we create the connection:" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "bit1.get() = 'off'\n" ] } ], "source": [ "bit1 = EpicsSignal(f\"{ioc}gp:bit1\", name=\"bit1\", string=True)\n", "bit1.wait_for_connection()\n", "print(f\"{bit1.get() = }\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The `read()` method (used by data acquisition) shows both *value* and the *timestamp* that value was received from EPICS. The timestamp is recorded by Python and is absolute number of seconds since January 1, 1970 GMT." ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'bit1': {'value': 'off', 'timestamp': 1629399463.264405}}" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "bit1.read()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## `EpicsSignalRO`\n", "\n", "The `gp:UPTIME` PV tells us how long the IOC has been running. This PV contains information that we cannot change since it reports a value that only the IOC knows. Create an `uptime` object with the `EpicsSignalRO` class from `ophyd`:" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "uptime.get() = '04:47:37'\n" ] } ], "source": [ "from ophyd import EpicsSignalRO\n", "uptime = EpicsSignalRO(f\"{ioc}UPTIME\", name=\"uptime\")\n", "uptime.wait_for_connection()\n", "print(f\"{uptime.get() = }\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## `Device` and `Component`\n", "\n", "We may have a group of PVs that are related in some way. It is possible to organize the Python objects into a larger structure called an `ophyd.Device` where each of the `EpicsSignal` objects is an `ophyd.Component`. Import the ophyd structures next:" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [], "source": [ "from ophyd import Component, Device" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Build a structure that associates these PVs and (hypothetical) uses:\n", "\n", "PV | how it is used | attribute\n", "--- | --- | ---\n", "`gp:gp:bit1` | on/off control | `enable`\n", "`gp:gp:float1` | desired temperature | `setpoint`\n", "`gp:gp:float1.EGU` | temperature units | `setpoint_units`\n", "`gp:gp:text1` | short label | `label`\n", "\n", "To group these PVs together, a custom Python class must be created using `Device` as a base class and `Component` for each of the PVs.\n", "\n", "To make this class more useful, we omit the IOC prefix (the first `gp:`) from our class. We'll use the IOC prefix later, when we create the Python object for this structure." ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [], "source": [ "class MyGroup(Device):\n", " enable = Component(EpicsSignal, \"gp:bit1\")\n", " setpoint = Component(EpicsSignal, \"gp:float1\")\n", " units = Component(EpicsSignal, \"gp:float1.EGU\")\n", " label = Component(EpicsSignal, \"gp:text1\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Use this structure to connect with all the PVs (at once) using the same type of command as with `EpicsSignal` above." ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [], "source": [ "thing = MyGroup(ioc, name=\"thing\")\n", "thing.wait_for_connection()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Show all the values at once using the `read()` method." ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "OrderedDict([('thing_enable', {'value': 0, 'timestamp': 1629399463.264405}),\n", " ('thing_setpoint',\n", " {'value': 521.7482, 'timestamp': 1629399413.336128}),\n", " ('thing_units',\n", " {'value': 'Rankine', 'timestamp': 1629399413.336128}),\n", " ('thing_label', {'value': '', 'timestamp': 631152000.0})])" ] }, "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ "thing.read()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Set the temperature units and set point:" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "OrderedDict([('thing_enable', {'value': 0, 'timestamp': 1629399463.264405}),\n", " ('thing_setpoint',\n", " {'value': 521.7482, 'timestamp': 1629399463.519874}),\n", " ('thing_units',\n", " {'value': 'Rankine', 'timestamp': 1629399463.519874}),\n", " ('thing_label', {'value': '', 'timestamp': 631152000.0})])" ] }, "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ "thing.setpoint.put(521.7482)\n", "thing.units.put(\"Rankine\")\n", "thing.read()" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "set point: 521.75 Rankine\n" ] } ], "source": [ "print(f\"set point: {thing.setpoint.get():.2f} {thing.units.get()}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "`Device` structures can be nested to make even more complex structures. For example:" ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "OrderedDict([('stack_basic_enable',\n", " {'value': 0, 'timestamp': 1629399463.264405}),\n", " ('stack_basic_setpoint',\n", " {'value': 521.7482, 'timestamp': 1629399463.519874}),\n", " ('stack_basic_units',\n", " {'value': 'Rankine', 'timestamp': 1629399463.519874}),\n", " ('stack_basic_label', {'value': '', 'timestamp': 631152000.0}),\n", " ('stack_reading', {'value': 0.0, 'timestamp': 631152000.0}),\n", " ('stack_abstract', {'value': '', 'timestamp': 631152000.0})])" ] }, "execution_count": 16, "metadata": {}, "output_type": "execute_result" } ], "source": [ "class Stack(Device):\n", " basic = Component(MyGroup, \"\")\n", " reading = Component(EpicsSignal, \"gp:float2\")\n", " abstract = Component(EpicsSignal, \"gp:longtext2\", string=True)\n", "\n", "stack = Stack(ioc, name=\"stack\")\n", "stack.wait_for_connection()\n", "stack.read()" ] } ], "metadata": { "interpreter": { "hash": "fa399ef8ed4fbc3b7fe63ebf4307839a170374bf77134d519fcb3b724ac0582b" }, "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.8.8" } }, "nbformat": 4, "nbformat_minor": 4 }