diff --git a/README.md b/README.md
index 96b4dce10..77c262bf9 100644
--- a/README.md
+++ b/README.md
@@ -9,8 +9,7 @@
as well as a straightforward Python framework to build complex applications.
- « Explore PyDM docs and tutorials »
-
+ « Explore PyDM docs and tutorials »
Report bug
·
diff --git a/docs/source/conf.py b/docs/source/conf.py
index ab90bdd6b..46eb3c4e2 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -56,8 +56,8 @@
# General information about the project.
project = "PyDM"
-copyright = "2016, mgibbs, hhslepicka, trendahl, zlentz"
-author = "mgibbs, hhslepicka, trendahl, zlentz"
+copyright = "2023, hhslepicka, trendahl, zlentz, yektay, nstelter"
+author = "mgibbs, hhslepicka, trendahl, zlentz, yektay, nstelter"
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
@@ -73,7 +73,7 @@
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
-language = None
+language = "en"
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
diff --git a/docs/source/installation.rst b/docs/source/installation.rst
index e084fe8f0..847a272a6 100644
--- a/docs/source/installation.rst
+++ b/docs/source/installation.rst
@@ -1,3 +1,5 @@
+.. _Install:
+
=========================
Installation
=========================
diff --git a/docs/source/tutorials/action/designer_expert.rst b/docs/source/tutorials/action/designer_expert.rst
index b6f90781e..d20e34eab 100644
--- a/docs/source/tutorials/action/designer_expert.rst
+++ b/docs/source/tutorials/action/designer_expert.rst
@@ -5,13 +5,7 @@ Expert Motor Screen
.. important::
- **Check-list:**
-
- * Make sure that you have your :ref:`Environment ` properly configured.
- * That your :ref:`VirtualMachine` is up and ready.
- * That the :ref:`Python environment ` is set.
- * That all :ref:`three IOCs ` are running.
-
+ * Make sure the PCASpy tutorial server is :ref:`running `
For this screen we will present detailed information to the user for the motors.
Also, to ensure that we can re-use this screen in other displays, it will be
@@ -312,4 +306,4 @@ The finished result will look like this:
:alt: Expert Motor Screen
.. note::
- You can download this file using :download:`this link `.
\ No newline at end of file
+ You can download this file using :download:`this link <../../../../examples/tutorial/expert_motor.ui>`.
\ No newline at end of file
diff --git a/docs/source/tutorials/action/designer_inline.rst b/docs/source/tutorials/action/designer_inline.rst
index 95e5c5be2..858cfe04c 100644
--- a/docs/source/tutorials/action/designer_inline.rst
+++ b/docs/source/tutorials/action/designer_inline.rst
@@ -5,12 +5,7 @@ Inline Motor Screen
.. important::
- **Check-list:**
-
- * Make sure that you have your :ref:`Environment ` properly configured.
- * That your :ref:`VirtualMachine` is up and ready.
- * That the :ref:`Python environment ` is set.
- * That all :ref:`three IOCs ` are running.
+ * Make sure the PCASpy tutorial server is :ref:`running `
For this screen, we want to present useful information to the user to operate
the motors, and also provide a way for them to access other less-commonly-used parameters via an "Expert" screen. To make this screen re-usable in other displays, it will be necessary
@@ -284,4 +279,4 @@ The finished result will look like this:
:alt: Inline Motor Screen
.. note::
- You can download this file using :download:`this link `.
+ You can download this file using :download:`this link <../../../../examples/tutorial/inline_motor.ui>`.
diff --git a/docs/source/tutorials/action/designer_main.rst b/docs/source/tutorials/action/designer_main.rst
index c1f60d1c2..901a722c3 100644
--- a/docs/source/tutorials/action/designer_main.rst
+++ b/docs/source/tutorials/action/designer_main.rst
@@ -5,13 +5,7 @@ Main Screen
.. important::
- **Check-list:**
-
- * Make sure that you have your :ref:`Environment ` properly configured.
- * That your :ref:`VirtualMachine` is up and ready.
- * That the :ref:`Python environment ` is set.
- * That all :ref:`three IOCs ` are running.
-
+ * Make sure the PCASpy tutorial server is :ref:`running `
This will be the main piece of our Beam Positioning application and will group the other
components of this tutorial.
@@ -256,5 +250,10 @@ The finished result will look like this:
:alt: Main Application Screen
.. note::
- You can download this file using :download:`this link `.
+ Purple borders will appear around any widgets that have "Alarm Sensitive Border" enabled.
+ These can be removed by simply unchecking the setting. (for the purposes of this tutorial,
+ these borders are not significant and can be in either the on or off state)
+
+.. note::
+ You can download this file using :download:`this link <../../../../examples/tutorial/main.ui>`.
diff --git a/docs/source/tutorials/action/little_code.rst b/docs/source/tutorials/action/little_code.rst
index 7129f04c2..474ade068 100644
--- a/docs/source/tutorials/action/little_code.rst
+++ b/docs/source/tutorials/action/little_code.rst
@@ -5,12 +5,7 @@ Adding Code into the Main Display
.. important::
- **Check-list:**
-
- * Make sure that you have your :ref:`Environment ` properly configured.
- * That your :ref:`VirtualMachine` is up and ready.
- * That the :ref:`Python environment ` is set.
- * That all :ref:`three IOCs ` are running.
+ * Make sure the PCASpy tutorial server is :ref:`running `
For this particular application it would be of interest to not only see the beam
image on the screen, but to also calculate the maximum point on the image and display
@@ -145,4 +140,4 @@ This is accomplished by subclassing `pydm.Display` (See :ref:`Display` for more
:align: center
.. note::
- You can download this file using :download:`this link `.
\ No newline at end of file
+ You can download this file using :download:`this link `.
\ No newline at end of file
diff --git a/docs/source/tutorials/action/python.rst b/docs/source/tutorials/action/python.rst
index 6d2d0f96b..6d654b844 100644
--- a/docs/source/tutorials/action/python.rst
+++ b/docs/source/tutorials/action/python.rst
@@ -5,12 +5,7 @@ Making Pure Python Displays
.. important::
- **Check-list:**
-
- * Make sure that you have your :ref:`Environment ` properly configured.
- * That your :ref:`VirtualMachine` is up and ready.
- * That the :ref:`Python environment ` is set.
- * That all :ref:`three IOCs ` are running.
+ * Make sure the PCASpy tutorial server is :ref:`running `
As we saw in the :ref:`A Word About Python Display ` section, it is
possible to make displays using Python code and a .ui file from Qt Designer.
@@ -37,7 +32,7 @@ Here is how it will look once we are done:
In order to simplify this tutorial, instead of using a database or other type
of service, the data to populate the list of motors will come from a simple text file
- named ``motor_db.txt`` that can be downloaded :download:`here `.
+ named ``motor_db.txt`` that can be downloaded :download:`here `.
* **Step 1.**
@@ -259,4 +254,4 @@ Here is how it will look once we are done:
:align: center
.. note::
- You can download this file using :download:`this link `.
\ No newline at end of file
+ You can download this file using :download:`this link `.
\ No newline at end of file
diff --git a/docs/source/tutorials/contrib/requests.rst b/docs/source/tutorials/contrib/requests.rst
index 87ffd3340..549a8c6d3 100644
--- a/docs/source/tutorials/contrib/requests.rst
+++ b/docs/source/tutorials/contrib/requests.rst
@@ -6,7 +6,7 @@ Follow the steps below to increase the chances of a quick resolution.
1. Is it a problem, a question or a request?
--------------------------------------------
-For questions about how to use PyDM, please refer to the :doc:`/contrib/help` page.
+For questions about how to use PyDM, please refer to the :doc:`help` page.
If you don't have a GitHub account we strongly encourage you to create one by following the 3 easy steps described on this `page `_.
@@ -18,10 +18,10 @@ Problems (not questions!) with the software or website can be reported as GitHub
- Typos or other mistakes in the Reference or elsewhere on the documentation
If you create a new issue that’s really just a clarifying question about how to use PyDM, we’ll close it and nicely ask
-you to visit the :doc:`/contrib/help` channels in the future.
+you to visit the :doc:`help` channels in the future.
Ideas or suggestions for enhancements should be posted as GitHub issues with the 'REQUEST: ' prefix to your issue title.
-When in doubt, start by reaching us through one of the :doc:`/contrib/help` channels.
+When in doubt, start by reaching us through one of the :doc:`help` channels.
2. Write helpfully
diff --git a/docs/source/tutorials/intro.rst b/docs/source/tutorials/intro.rst
index 68a185f27..287e4ca61 100644
--- a/docs/source/tutorials/intro.rst
+++ b/docs/source/tutorials/intro.rst
@@ -1,94 +1,42 @@
-.. _Environment:
+.. _Setup:
-Setting up the Environment
+Tutorial Setup
==========================
-.. _VirtualMachine:
-
-Virtual Machine
+PCASpy Server
---------------
-We provide a virtual machine disk that is the base for the application that will
-be developed during this tutorial.
-
-You can download the disk using this `Link `_.
-
-Using the Downloaded Disk
-^^^^^^^^^^^^^^^^^^^^^^^^^
-
-After downloading it, extract the ``.tar.gz`` file, create a new Virtual Machine at the virtualization client of your preference.
-
-The instructions below are for `Oracle VirtualBox `_ .
-Oracle VirtualBox is available for Windows, OS X and Linux hosts.
-
-This file is not a complete Virtual Machine dump that can be imported but instead a disk.
-
-In order to use this disk, start by creating a new virtual Machine, select Type as ``Linux`` and Version as ``Ubuntu (64-bit)``.
-Configure the amount of memory to use (something greater or equal 2048MB should do it.
-Make sure to select ``Use an existing virtual hard disk file.`` and select the extracted ``.vmdk`` file.
-
-.. figure:: /_static/tutorials/new_vm.png
- :scale: 100 %
- :align: center
- :alt: Create new VM
-
+A `PCASpy `_ server provides PVs for the tutorial files to read/write.
-Useful Virtual Machine Information
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+The server mimics some PVs of a motor and camera, and is located as follows:
+ .. code-block:: bash
-User Account
-++++++++++++
-======== ========
-Username Password
-======== ========
-user tutorial
-======== ========
+ examples/testing_ioc/pydm-tutorial-ioc
-.. _PythonEnv:
+Installing PCASpy from the documentation above and following the :ref:`pydm installation instructions` provides all needed prerequisites for this tutorial.
-Python Environment
-++++++++++++++++++
-
-On this machine we are using Miniconda to handle our Python environment and dependencies.
-To have access to the environmnet please do:
-
-.. code-block:: bash
-
- source activate tutorial
-
-.. _IOCS:
-
-Simulated EPICS IOCs
-++++++++++++++++++++
-
-This machine comes with simulated motors and cameras.
-The IOCs can be started through their launcher scripts available at:
-
-.. code-block:: bash
-
- cd ~/tutorial/iocs_launcher
-
- # For the AreaDetector (cameras) simulation use
- ./simDetector
+Using the PCASpy Server
+^^^^^^^^^^^^^^^^^^^^^^^^^
- # For the simulated motor axis use
- ./simMotor
+.. note::
+ You will need to export the following variable in each terminal that will run either the PCASpy server or pydm:
+ .. code-block:: bash
- # For the linking IOC
- ./simLinker
+ export EPICS_CA_MAX_ARRAY_BYTES=300000
-For AreaDetector (cameras):
+Run the server as follows:
+ .. code-block:: bash
-- The prefix for the PVs is ``13SIM1:`` so we have: ``13SIM1:cam1`` as well as ``13SIM1:cam2`` available.
+ ./examples/testing_ioc/pydm-tutorial-ioc
-For Motor Axis:
+In another terminal window, enable the sever's running state:
+ .. code-block:: bash
-- The prexif for the PVs is ``IOC:`` so we have: ``IOC:m1 .. IOC:m8``
+ caput IOC:Run 1
+The server will now be running and the tutorial files can access the necessary PV's.
-Creating your own environment
------------------------------
+In another (third) terminal window, the completed tutorial files can be ran as follows:
+ .. code-block:: bash
-If you decide to create your own environment and not use the Virtual Machine
-provided please refer to the `PyDM Documentation Website `_
-for an up-to-date dependency list as well as detailed installation instructions.
+ pydm .ui|.py
\ No newline at end of file
diff --git a/docs/source/utilities/index.rst b/docs/source/utilities/index.rst
index a0e23566d..cd7555fa7 100644
--- a/docs/source/utilities/index.rst
+++ b/docs/source/utilities/index.rst
@@ -10,9 +10,6 @@ General
.. automodule:: pydm.utilities
:members:
-.. automodule:: pydm.utilities.remove_protocol
- :members:
-
--------
Icons
--------
diff --git a/docs/source/widgets/analog_indicator.rst b/docs/source/widgets/analog_indicator.rst
index b70890e9e..7e93b5f70 100644
--- a/docs/source/widgets/analog_indicator.rst
+++ b/docs/source/widgets/analog_indicator.rst
@@ -25,6 +25,7 @@ backgroundSizeRate = 0.4.
:align: center
* Suggested Orientations
+
Horizontal with value displayed on the right.
Vertical with value displayed on bottom.
@@ -39,6 +40,7 @@ There are a few methods of not drawing alarm regions.
1. Set the alarm to the corresponding limit. Do not set the alarm to outside of the limits, this will cause drawing errors.
2. Set userUpperMajorAlarm = userLowerMajorAlarm = 0. Or set userUpperMinorAlarm = userLowerMinorAlarm = 0.
3. If any alarm value is set to nan (not a number), those regions won't draw. Setting an alarm value to nan is not possible in designer.
+
.. figure:: /_static/widgets/analog_indicator/no_upper_minor.png
:scale: 100%
:align: center
diff --git a/docs/source/widgets/archiver_timeplot.rst b/docs/source/widgets/archiver_timeplot.rst
index 0ad8986a0..2a761fda8 100644
--- a/docs/source/widgets/archiver_timeplot.rst
+++ b/docs/source/widgets/archiver_timeplot.rst
@@ -46,7 +46,7 @@ be plotted as bars to show the full range of data represented by each point. As
of 365, a request for a year of data for a PV that updates every second would return roughly
365 points each of which will contain the min and max of that day's data to plot the full range represented.
-.. figure:: /_static/widgets/archiver_time_plot/archiver_time_plot.gif
+.. figure:: /_static/widgets/archiver_time_plot/archiver_plot.gif
:scale: 100 %
:align: center
:alt: Requesting additional data from a live plot
diff --git a/docs/source/widgets/index.rst b/docs/source/widgets/index.rst
index 1c1ccdd69..6ddc6d423 100644
--- a/docs/source/widgets/index.rst
+++ b/docs/source/widgets/index.rst
@@ -17,6 +17,8 @@ Display Widgets
related_display_button.rst
scale.rst
symbol.rst
+ analog_indicator.rst
+ nt_table.rst
Input Widgets
-------------
@@ -62,6 +64,13 @@ Drawing Widgets
drawing.rst
+Base Widgets
+------------
+.. toctree::
+ :maxdepth: 1
+
+ PyDMWidget.rst
+
Utilities
---------
diff --git a/examples/testing_ioc/pydm-tutorial-ioc b/examples/testing_ioc/pydm-tutorial-ioc
new file mode 100755
index 000000000..0f83d50a9
--- /dev/null
+++ b/examples/testing_ioc/pydm-tutorial-ioc
@@ -0,0 +1,261 @@
+#!/usr/bin/env python
+import os
+import threading
+import numpy
+import time
+
+from pcaspy import Driver, SimpleServer
+
+""" This file provides the server needed for for providing PVs to run the tutorial.
+ You can follow along the tutorial here: https://slaclab.github.io/pydm/tutorials/index.html
+ The server mimics variables used by the tutorial, which mimic a simulated camera and motor.
+ Mimicking this behavior is done (over running simulator programs) in order to minimize the overhead
+ to get the tutorial up-and-running, and since the simulated data is not necessarily needed to learn
+ how to make a PyDM user-interface.
+"""
+
+MAX_POINTS = 1000
+FREQUENCY = 1000
+AMPLITUDE = 1.0
+NUM_DIVISIONS = 10
+MIN_UPDATE_TIME = 0.001
+IMAGE_SIZE = 512
+MESSAGE = "PyDM Rocks!"
+
+prefix = "IOC:"
+pvdb = {
+ "Run": {"type": "enum", "enums": ["STOP", "RUN"], "asg": "default"},
+ "ReadOnly": {"type": "enum", "enums": ["FALSE", "TRUE"], "value": 0},
+ "XPos": {"prec": 2, "value": 0.0, "asg": "default"},
+ "YPos": {"prec": 2, "value": 0.0, "asg": "default"},
+ "Image": {
+ "type": "char",
+ "count": IMAGE_SIZE**2,
+ "value": numpy.zeros(IMAGE_SIZE**2, dtype=numpy.uint8),
+ "asg": "default",
+ },
+ "ImageWidth": {"type": "int", "value": IMAGE_SIZE, "asg": "default"},
+ # m1
+ "m1.DESC": {"type": "string", "value": "Motor X", "asg": "default"},
+ "m1": {"type": "float", "unit": "degrees", "prec": 1, "value": 500.0, "asg": "default"},
+ "m1.VAL": {"type": "float", "unit": "degrees", "prec": 5, "value": 500.0, "asg": "default"},
+ "m1.RBV": {"type": "float", "prec": 5, "unit": "degrees", "value": 500.0, "asg": "default"},
+ "m1.MOVN": {"type": "int", "value": 0, "asg": "default"},
+ "m1.STOP": {"type": "int", "value": 0, "asg": "default"},
+ "m1.ACCL": {"type": "float", "prec": 3, "unit": "sec", "value": 0.002, "asg": "default"},
+ "m1.VELO": {"type": "float", "prec": 1, "unit": "degrees", "value": 100.0, "asg": "default"},
+ # m2
+ "m2.DESC": {"type": "string", "value": "Motor Y", "asg": "default"},
+ "m2": {"type": "float", "prec": 1, "unit": "degrees", "value": 500.0, "asg": "default"},
+ "m2.VAL": {"type": "float", "unit": "degrees", "prec": 5, "value": 500.0, "asg": "default"},
+ "m2.RBV": {"type": "float", "prec": 5, "value": 500.0, "unit": "degrees", "asg": "default"},
+ "m2.MOVN": {"type": "int", "value": 0, "asg": "default"},
+ "m2.STOP": {"type": "int", "value": 0, "asg": "default"},
+ "m2.ACCL": {"type": "float", "prec": 3, "value": 0.002, "unit": "sec", "asg": "default"},
+ "m2.VELO": {"type": "float", "prec": 1, "value": 100.0, "unit": "degrees", "asg": "default"},
+ # m3
+ "m3.DESC": {"type": "string", "value": "Motor 3", "asg": "default"},
+ "m3": {"type": "float", "prec": 1, "unit": "degrees", "value": 500.0, "asg": "default"},
+ "m3.VAL": {"type": "float", "unit": "degrees", "prec": 5, "value": 500.0, "asg": "default"},
+ "m3.RBV": {"type": "float", "prec": 5, "value": 500.0, "unit": "degrees", "asg": "default"},
+ "m3.MOVN": {"type": "int", "value": 0, "asg": "default"},
+ "m3.STOP": {"type": "int", "value": 0, "asg": "default"},
+ "m3.ACCL": {"type": "float", "prec": 3, "value": 0.002, "unit": "sec", "asg": "default"},
+ "m3.VELO": {"type": "float", "prec": 1, "value": 100.0, "unit": "degrees", "asg": "default"},
+ # m4
+ "m4.DESC": {"type": "string", "value": "Motor 4", "asg": "default"},
+ "m4": {"type": "float", "prec": 1, "unit": "degrees", "value": 500.0, "asg": "default"},
+ "m4.VAL": {"type": "float", "unit": "degrees", "prec": 5, "value": 500.0, "asg": "default"},
+ "m4.RBV": {"type": "float", "prec": 5, "value": 500.0, "unit": "degrees", "asg": "default"},
+ "m4.MOVN": {"type": "int", "value": 0, "asg": "default"},
+ "m4.STOP": {"type": "int", "value": 0, "asg": "default"},
+ "m4.ACCL": {"type": "float", "prec": 3, "value": 0.002, "unit": "sec", "asg": "default"},
+ "m4.VELO": {"type": "float", "prec": 1, "value": 100.0, "unit": "degrees", "asg": "default"},
+ # m5
+ "m5.DESC": {"type": "string", "value": "Motor 5", "asg": "default"},
+ "m5": {"type": "float", "prec": 1, "unit": "degrees", "value": 500.0, "asg": "default"},
+ "m5.VAL": {"type": "float", "unit": "degrees", "prec": 5, "value": 500.0, "asg": "default"},
+ "m5.RBV": {"type": "float", "prec": 5, "value": 500.0, "unit": "degrees", "asg": "default"},
+ "m5.MOVN": {"type": "int", "value": 0, "asg": "default"},
+ "m5.STOP": {"type": "int", "value": 0, "asg": "default"},
+ "m5.ACCL": {"type": "float", "prec": 3, "value": 0.002, "unit": "sec", "asg": "default"},
+ "m5.VELO": {"type": "float", "prec": 1, "value": 100.0, "unit": "degrees", "asg": "default"},
+ # m6
+ "m6.DESC": {"type": "string", "value": "Motor 6", "asg": "default"},
+ "m6": {"type": "float", "prec": 1, "unit": "degrees", "value": 500.0, "asg": "default"},
+ "m6.VAL": {"type": "float", "unit": "degrees", "prec": 5, "value": 500.0, "asg": "default"},
+ "m6.RBV": {"type": "float", "prec": 5, "value": 500.0, "unit": "degrees", "asg": "default"},
+ "m6.MOVN": {"type": "int", "value": 0, "asg": "default"},
+ "m6.STOP": {"type": "int", "value": 0, "asg": "default"},
+ "m6.ACCL": {"type": "float", "prec": 3, "value": 0.002, "unit": "sec", "asg": "default"},
+ "m6.VELO": {"type": "float", "prec": 1, "value": 100.0, "unit": "degrees", "asg": "default"},
+ # m7
+ "m7.DESC": {"type": "string", "value": "Motor 7", "asg": "default"},
+ "m7": {"type": "float", "prec": 1, "unit": "degrees", "value": 500.0, "asg": "default"},
+ "m7.VAL": {"type": "float", "unit": "degrees", "prec": 5, "value": 500.0, "asg": "default"},
+ "m7.RBV": {"type": "float", "prec": 5, "value": 500.0, "unit": "degrees", "asg": "default"},
+ "m7.MOVN": {"type": "int", "value": 0, "asg": "default"},
+ "m7.STOP": {"type": "int", "value": 0, "asg": "default"},
+ "m7.ACCL": {"type": "float", "prec": 3, "value": 0.002, "unit": "sec", "asg": "default"},
+ "m7.VELO": {"type": "float", "prec": 1, "value": 100.0, "unit": "degrees", "asg": "default"},
+ # m8
+ "m8.DESC": {"type": "string", "value": "Motor 8", "asg": "default"},
+ "m8": {"type": "float", "prec": 1, "unit": "degrees", "value": 500.0, "asg": "default"},
+ "m8.VAL": {"type": "float", "unit": "degrees", "prec": 5, "value": 500.0, "asg": "default"},
+ "m8.RBV": {"type": "float", "prec": 5, "value": 500.0, "unit": "degrees", "asg": "default"},
+ "m8.MOVN": {"type": "int", "value": 0, "asg": "default"},
+ "m8.STOP": {"type": "int", "value": 0, "asg": "default"},
+ "m8.ACCL": {"type": "float", "prec": 3, "value": 0.002, "unit": "sec", "asg": "default"},
+ "m8.VELO": {"type": "float", "prec": 1, "value": 100.0, "unit": "degrees", "asg": "default"},
+}
+
+
+def gaussian_2d(x, y, x0, y0, xsig, ysig):
+ return numpy.exp(-0.5 * (((x - x0) / xsig) ** 2 + ((y - y0) / ysig) ** 2))
+
+
+class myDriver(Driver):
+ def __init__(self):
+ Driver.__init__(self)
+ self.eid = threading.Event()
+ self.tid = threading.Thread(target=self.runSimScope)
+ self.tid.setDaemon(True)
+ self.tid.start()
+
+ self.motorXThread = threading.Thread(
+ target=self.updateMotor,
+ args=(
+ "m1",
+ "XPos",
+ ),
+ )
+ self.motorXThread.setDaemon(True)
+ self.motorXThread.start()
+
+ self.motorYThread = threading.Thread(
+ target=self.updateMotor,
+ args=(
+ "m2",
+ "YPos",
+ ),
+ )
+ self.motorYThread.setDaemon(True)
+ self.motorYThread.start()
+
+ for i in range(3, 9):
+ currMotorName = "m" + str(i)
+ motorCurrThread = threading.Thread(
+ target=self.updateMotor,
+ args=(
+ currMotorName,
+ "NA",
+ ),
+ )
+ motorCurrThread.setDaemon(True)
+ motorCurrThread.start()
+
+ def updateMotor(self, motorVarName, axisVarName):
+ # mimic the functionality of fully-simulated motor and camera
+
+ sleepTime = 0.2 # set arbitrarily to match timing when fully-simulated
+ motorRbvName = motorVarName + ".RBV"
+ motorMovingVarName = motorVarName + ".MOVN"
+ # set to avoid button having purple "uninit" color
+ self.setParam(motorMovingVarName, 0)
+ motorStopVarName = motorVarName + ".STOP"
+ motorValVarName = motorVarName + ".VAL"
+
+ motorRbv = self.getParam(motorRbvName)
+ while True:
+ time.sleep(sleepTime)
+ motorParam = self.getParam(motorVarName)
+
+ if motorParam != motorRbv: # need to move motor
+ self.setParam(motorMovingVarName, 1)
+
+ # for Motor X: 'Tw +10' = move right, 'Tw -10' = move left
+ # for Motor Y: 'Tw -10' = move up, 'Tw +10' = move down left
+ motorMoveAmount = 10 if motorRbv < motorParam else -10
+ # so Motor Y buttons move as expected
+ if axisVarName == "YPos":
+ imageMoveAmount = -0.1 if motorRbv < motorParam else 0.1
+ else:
+ imageMoveAmount = 0.1 if motorRbv < motorParam else -0.1
+
+ # do movement in increments, updating displayed value and sleeping inbetween
+ while motorRbv != motorParam:
+ currStopVal = self.getParam(motorStopVarName)
+ if currStopVal:
+ break
+
+ # update displayed values
+
+ # allow for setting values not divisible by 10
+ if motorParam > 0:
+ motorRbv = (
+ motorParam if (motorRbv + motorMoveAmount > motorParam) else motorRbv + motorMoveAmount
+ )
+ else:
+ motorRbv = (
+ motorParam if (motorRbv + motorMoveAmount < motorParam) else motorRbv + motorMoveAmount
+ )
+ self.setParam(motorRbvName, motorRbv)
+ self.setParam(motorValVarName, motorRbv)
+
+ # only update axis for m1(XPos) and m2(YPos)
+ if axisVarName != "NA":
+ axis_pos = self.getParam(axisVarName)
+ self.setParam(axisVarName, axis_pos + imageMoveAmount)
+
+ self.updatePVs()
+ time.sleep(sleepTime)
+
+ self.setParam(motorStopVarName, 0)
+ # if stopped, set motorVar line-edit to the motorRBV text-field value
+ # (motor was stopped before reaching the originally entered value)
+ if motorParam != motorRbv:
+ self.setParam(motorVarName, motorRbv)
+
+ self.setParam(motorMovingVarName, 0)
+ self.updatePVs()
+
+ def runSimScope(self):
+ # simulate scope waveform
+ x = numpy.linspace(-5.0, 5.0, IMAGE_SIZE)
+ y = numpy.linspace(-5.0, 5.0, IMAGE_SIZE)
+ xgrid, ygrid = numpy.meshgrid(x, y)
+
+ while True:
+ # Generate the image data
+ x0 = 0.1 * (numpy.random.rand() - 0.5) + self.getParam("XPos")
+ y0 = 0.1 * (numpy.random.rand() - 0.5) - self.getParam("YPos")
+ xsig = 0.6
+ ysig = 0.2
+ z = gaussian_2d(xgrid, ygrid, x0, y0, xsig, ysig)
+ image_data = numpy.abs(256.0 * (z)).flatten(order="C").astype(numpy.uint8, copy=False)
+ self.setParam("Image", image_data)
+
+ # do updates so clients see the changes
+ self.updatePVs()
+
+
+if __name__ == "__main__":
+ try:
+ print("Starting testing-ioc")
+ print("To start processing records do: caput " + prefix + "Run 1")
+ server = SimpleServer()
+ server.initAccessSecurityFile(
+ os.path.join(os.path.dirname(os.path.realpath(__file__)), "access_rules.as"), P=prefix
+ )
+ server.createPV(prefix, pvdb)
+ driver = myDriver()
+
+ # Manually set the ReadOnly PV to force access rule calculation.
+ # You can set ReadOnly to 1 to disable write access on all PVs.
+ driver.setParam("ReadOnly", 0)
+
+ # process CA transactions
+ while True:
+ server.process(0.03)
+ except KeyboardInterrupt:
+ print("\nInterrupted... finishing testing-ioc")
diff --git a/examples/tutorial/all_motors.py b/examples/tutorial/all_motors.py
new file mode 100644
index 000000000..56ebc4a30
--- /dev/null
+++ b/examples/tutorial/all_motors.py
@@ -0,0 +1,146 @@
+import os
+import json
+from pydm import Display
+from qtpy.QtWidgets import (
+ QVBoxLayout,
+ QHBoxLayout,
+ QGroupBox,
+ QLabel,
+ QLineEdit,
+ QPushButton,
+ QScrollArea,
+ QFrame,
+ QApplication,
+ QWidget,
+)
+from qtpy import QtCore
+from pydm.widgets import PyDMEmbeddedDisplay
+
+
+class AllMotorsDisplay(Display):
+ def __init__(self, parent=None, args=[], macros=None):
+ super(AllMotorsDisplay, self).__init__(parent=parent, args=args, macros=None)
+ # Placeholder for data to filter
+ self.data = []
+ # Reference to the PyDMApplication
+ self.app = QApplication.instance()
+ # Assemble the Widgets
+ self.setup_ui()
+ # Load data from file
+ self.load_data()
+
+ def minimumSizeHint(self):
+ # This is the default recommended size
+ # for this screen
+ return QtCore.QSize(750, 120)
+
+ def ui_filepath(self):
+ # No UI file is being used
+ return None
+
+ def setup_ui(self):
+ # Create the main layout
+ main_layout = QVBoxLayout()
+ self.setLayout(main_layout)
+
+ # Create a Label to be the title
+ lbl_title = QLabel("Motors Diagnostic")
+ # Add some StyleSheet to it
+ lbl_title.setStyleSheet(
+ "\
+ QLabel {\
+ qproperty-alignment: AlignCenter;\
+ border: 1px solid #FF17365D;\
+ border-top-left-radius: 15px;\
+ border-top-right-radius: 15px;\
+ background-color: #FF17365D;\
+ padding: 5px 0px;\
+ color: rgb(255, 255, 255);\
+ max-height: 25px;\
+ font-size: 14px;\
+ }"
+ )
+
+ # Add the title label to the main layout
+ main_layout.addWidget(lbl_title)
+
+ # Create the Search Panel layout
+ search_layout = QHBoxLayout()
+
+ # Create a GroupBox with "Filtering" as Title
+ gb_search = QGroupBox(parent=self)
+ gb_search.setTitle("Filtering")
+ gb_search.setLayout(search_layout)
+
+ # Create a label, line edit and button for filtering
+ lbl_search = QLabel(text="Filter: ")
+ self.txt_filter = QLineEdit()
+ self.txt_filter.returnPressed.connect(self.do_search)
+ btn_search = QPushButton()
+ btn_search.setText("Search")
+ btn_search.clicked.connect(self.do_search)
+
+ # Add the created widgets to the layout
+ search_layout.addWidget(lbl_search)
+ search_layout.addWidget(self.txt_filter)
+ search_layout.addWidget(btn_search)
+
+ # Add the Groupbox to the main layout
+ main_layout.addWidget(gb_search)
+
+ # Create the Results Layout
+ self.results_layout = QVBoxLayout()
+ self.results_layout.setContentsMargins(0, 0, 0, 0)
+
+ # Create a Frame to host the results of search
+ self.frm_result = QFrame(parent=self)
+ self.frm_result.setLayout(self.results_layout)
+
+ # Create a ScrollArea so we can properly handle
+ # many entries
+ scroll_area = QScrollArea(parent=self)
+ scroll_area.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
+ scroll_area.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+ scroll_area.setWidgetResizable(True)
+
+ # Add the Frame to the scroll area
+ scroll_area.setWidget(self.frm_result)
+
+ # Add the scroll area to the main layout
+ main_layout.addWidget(scroll_area)
+
+ def load_data(self):
+ # Extract the directory of this file...
+ base_dir = os.path.dirname(os.path.realpath(__file__))
+ # Concatenate the directory with the file name...
+ data_file = os.path.join(base_dir, "motor_db.txt")
+ # Open the file so we can read the data...
+ with open(data_file, "r") as f:
+ # For each line in the file...
+ for entry in f.readlines():
+ # Append to the list of data...
+ self.data.append(entry[:-1])
+
+ def do_search(self):
+ # For each widget inside the results frame, lets destroy them
+ for widget in self.frm_result.findChildren(QWidget):
+ widget.setParent(None)
+ widget.deleteLater()
+
+ # Grab the filter text
+ filter_text = self.txt_filter.text()
+
+ # For every entry in the dataset...
+ for entry in self.data:
+ # Check if they match our filter
+ if filter_text.upper() not in entry.upper():
+ continue
+ # Create a PyDMEmbeddedDisplay for this entry
+ disp = PyDMEmbeddedDisplay(parent=self)
+ disp.macros = json.dumps({"MOTOR": entry})
+ disp.filename = "inline_motor.ui"
+ disp.setMinimumWidth(700)
+ disp.setMinimumHeight(40)
+ disp.setMaximumHeight(100)
+ # Add the Embedded Display to the Results Layout
+ self.results_layout.addWidget(disp)
diff --git a/examples/tutorial/expert_motor.ui b/examples/tutorial/expert_motor.ui
new file mode 100644
index 000000000..fafcf5374
--- /dev/null
+++ b/examples/tutorial/expert_motor.ui
@@ -0,0 +1,373 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 447
+ 217
+
+
+
+ Engineer Screen: ${MOTOR}
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+ -
+
+
+ 0
+
+
-
+
+
+ QLabel {
+ qproperty-alignment: AlignCenter;
+ border: 1px solid #FF17365D;
+ border-top-left-radius: 15px;
+ border-top-right-radius: 15px;
+ background-color: #FF17365D;
+ padding: 5px 0px;
+ color: rgb(255, 255, 255);
+ max-height: 25px;
+ font-size: 14px;
+}
+
+
+
+ Configuring Motor: ${MOTOR}
+
+
+
+ -
+
+
+ QFrame#frame{
+ border: 1px solid #FF17365D;
+ border-bottom-left-radius: 15px;
+ border-bottom-right-radius: 15px;
+}
+
+
+ QFrame::StyledPanel
+
+
+ QFrame::Raised
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter
+
+
+ 10
+
+
+ 10
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
-
+
+
+ Description:
+
+
+
+ -
+
+
+
+
+
+
+
+
+ 0
+
+
+ false
+
+
+ true
+
+
+ false
+
+
+ false
+
+
+
+
+
+ false
+
+
+ ca://${MOTOR}.DESC
+
+
+ PyDMLineEdit::String
+
+
+
+ -
+
+
+ Position:
+
+
+
+ -
+
+
+
+ 150
+ 16777215
+
+
+
+
+
+
+
+
+
+ 0
+
+
+ true
+
+
+ true
+
+
+ false
+
+
+ false
+
+
+
+
+
+ false
+
+
+ ca://${MOTOR}.VAL
+
+
+ PyDMLineEdit::Decimal
+
+
+
+ -
+
+
+ Readback:
+
+
+
+ -
+
+
+
+
+
+
+
+
+ 0
+
+
+ true
+
+
+ true
+
+
+ false
+
+
+ false
+
+
+
+
+
+ ca://${MOTOR}.RBV
+
+
+ false
+
+
+ PyDMLabel::Decimal
+
+
+
+ -
+
+
+ Velocity:
+
+
+
+ -
+
+
+
+ 150
+ 16777215
+
+
+
+
+
+
+
+
+
+ 0
+
+
+ true
+
+
+ true
+
+
+ false
+
+
+ false
+
+
+
+
+
+ false
+
+
+ ca://${MOTOR}.VELO
+
+
+ PyDMLineEdit::Decimal
+
+
+
+ -
+
+
+ Acceleration:
+
+
+
+ -
+
+
+
+ 150
+ 16777215
+
+
+
+
+
+
+
+
+
+ 0
+
+
+ true
+
+
+ true
+
+
+ false
+
+
+ false
+
+
+
+
+
+ false
+
+
+ ca://${MOTOR}.ACCL
+
+
+ PyDMLineEdit::Decimal
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PyDMLabel
+ QLabel
+
+
+
+ PyDMLineEdit
+ QLineEdit
+
+
+
+
+
+
diff --git a/examples/tutorial/inline_motor.ui b/examples/tutorial/inline_motor.ui
new file mode 100644
index 000000000..e7c4aad98
--- /dev/null
+++ b/examples/tutorial/inline_motor.ui
@@ -0,0 +1,565 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 700
+ 32
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 700
+ 32
+
+
+
+
+ 16777215
+ 38
+
+
+
+ Form
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+ -
+
+
+ 5
+
+
+ 5
+
+
+ 10
+
+
+ 5
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+
+ 75
+ 0
+
+
+
+
+
+
+
+
+
+ 0
+
+
+ false
+
+
+ true
+
+
+ false
+
+
+ false
+
+
+
+
+
+ ca://${MOTOR}.RBV
+
+
+ false
+
+
+ PyDMLabel::Decimal
+
+
+
+ -
+
+
+
+ 50
+ 0
+
+
+
+
+ 50
+ 16777215
+
+
+
+
+
+
+
+ Basic PushButton to send a fixed value.
+
+ The PyDMPushButton is meant to hold a specific value, and send that value
+ to a channel when it is clicked, much like the MessageButton does in EDM.
+ The PyDMPushButton works in two different modes of operation, first, a
+ fixed value can be given to the :attr:`.pressValue` attribute, whenever the
+ button is clicked a signal containing this value will be sent to the
+ connected channel. This is the default behavior of the button. However, if
+ the :attr:`.relativeChange` is set to True, the fixed value will be added
+ to the current value of the channel. This means that the button will
+ increment a channel by a fixed amount with every click, a consistent
+ relative move
+
+ Parameters
+ ----------
+ parent : QObject, optional
+ Parent of PyDMPushButton
+
+ label : str, optional
+ String to place on button
+
+ icon : QIcon, optional
+ An Icon to display on the PyDMPushButton
+
+ pressValue : int, float, str
+ Value to be sent when the button is clicked
+
+ relative : bool, optional
+ Choice to have the button perform a relative put, instead of always
+ setting to an absolute value
+
+ init_channel : str, optional
+ ID of channel to manipulate
+
+
+
+
+ Tw +10
+
+
+ ca://${MOTOR}
+
+
+ 10
+
+
+ true
+
+
+
+ -
+
+
+
+ 50
+ 0
+
+
+
+
+ 50
+ 16777215
+
+
+
+
+
+
+
+ Basic PushButton to send a fixed value.
+
+ The PyDMPushButton is meant to hold a specific value, and send that value
+ to a channel when it is clicked, much like the MessageButton does in EDM.
+ The PyDMPushButton works in two different modes of operation, first, a
+ fixed value can be given to the :attr:`.pressValue` attribute, whenever the
+ button is clicked a signal containing this value will be sent to the
+ connected channel. This is the default behavior of the button. However, if
+ the :attr:`.relativeChange` is set to True, the fixed value will be added
+ to the current value of the channel. This means that the button will
+ increment a channel by a fixed amount with every click, a consistent
+ relative move
+
+ Parameters
+ ----------
+ parent : QObject, optional
+ Parent of PyDMPushButton
+
+ label : str, optional
+ String to place on button
+
+ icon : QIcon, optional
+ An Icon to display on the PyDMPushButton
+
+ pressValue : int, float, str
+ Value to be sent when the button is clicked
+
+ relative : bool, optional
+ Choice to have the button perform a relative put, instead of always
+ setting to an absolute value
+
+ init_channel : str, optional
+ ID of channel to manipulate
+
+
+
+
+ Tw -10
+
+
+ ca://${MOTOR}
+
+
+ -10
+
+
+ true
+
+
+
+ -
+
+
+
+
+
+
+ Basic PushButton to send a fixed value.
+
+ The PyDMPushButton is meant to hold a specific value, and send that value
+ to a channel when it is clicked, much like the MessageButton does in EDM.
+ The PyDMPushButton works in two different modes of operation, first, a
+ fixed value can be given to the :attr:`.pressValue` attribute, whenever the
+ button is clicked a signal containing this value will be sent to the
+ connected channel. This is the default behavior of the button. However, if
+ the :attr:`.relativeChange` is set to True, the fixed value will be added
+ to the current value of the channel. This means that the button will
+ increment a channel by a fixed amount with every click, a consistent
+ relative move
+
+ Parameters
+ ----------
+ parent : QObject, optional
+ Parent of PyDMPushButton
+
+ label : str, optional
+ String to place on button
+
+ icon : QIcon, optional
+ An Icon to display on the PyDMPushButton
+
+ pressValue : int, float, str
+ Value to be sent when the button is clicked
+
+ relative : bool, optional
+ Choice to have the button perform a relative put, instead of always
+ setting to an absolute value
+
+ init_channel : str, optional
+ ID of channel to manipulate
+
+
+
+
+ background-color: red;
+
+
+ Stop
+
+
+ ca://${MOTOR}.STOP
+
+
+ 1
+
+
+
+ -
+
+
+
+ 125
+ 24
+
+
+
+
+ 125
+ 24
+
+
+
+
+
+
+
+ A QPushButton capable of opening a new Display at the same of at a
+ new window.
+
+ Parameters
+ ----------
+ init_channel : str, optional
+ The channel to be used by the widget.
+
+ filename : str, optional
+ The file to be opened
+
+
+
+ Engineer...
+
+
+ true
+
+
+ expert_motor.ui
+
+
+
+ {"MOTOR":"${MOTOR}"}
+
+
+
+ true
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 75
+ 0
+
+
+
+
+
+
+
+
+
+ 0
+
+
+ false
+
+
+ true
+
+
+ false
+
+
+ false
+
+
+
+
+
+ false
+
+
+ ca://${MOTOR}
+
+
+ PyDMLineEdit::Decimal
+
+
+
+ -
+
+
+
+ 100
+ 0
+
+
+
+
+ Arial
+ 75
+ true
+
+
+
+
+
+
+
+
+
+ 0
+
+
+ false
+
+
+ true
+
+
+ false
+
+
+ false
+
+
+
+
+
+ ca://${MOTOR}.DESC
+
+
+ false
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 32
+ 32
+
+
+
+
+ 32
+ 32
+
+
+
+
+
+
+
+ Widget for graphical representation of bits from an integer number
+ with support for Channels and more from PyDM
+
+ Parameters
+ ----------
+ parent : QWidget
+ The parent widget for the Label
+ init_channel : str, optional
+ The channel to be used by the widget.
+
+
+
+ false
+
+
+ ca://${MOTOR}.MOVN
+
+
+
+ 0
+ 255
+ 0
+
+
+
+
+ 100
+ 100
+ 100
+
+
+
+ Qt::Vertical
+
+
+ false
+
+
+ true
+
+
+ QTabWidget::East
+
+
+
+
+
+
+
+ false
+
+
+
+
+
+
+
+
+
+ PyDMLabel
+ QLabel
+
+
+
+ PyDMByteIndicator
+ QWidget
+
+
+
+ PyDMLineEdit
+ QLineEdit
+
+
+
+ PyDMPushButton
+ QPushButton
+
+
+
+ PyDMRelatedDisplayButton
+ QPushButton
+ pydm.widgets.related_display_button
+
+
+
+
+
diff --git a/examples/tutorial/main.py b/examples/tutorial/main.py
new file mode 100644
index 000000000..5188a3205
--- /dev/null
+++ b/examples/tutorial/main.py
@@ -0,0 +1,41 @@
+from os import path
+from pydm import Display
+from scipy.ndimage.measurements import maximum_position
+
+
+class BeamPositioning(Display):
+ def __init__(self, parent=None, args=None):
+ super(BeamPositioning, self).__init__(parent=parent, args=args)
+ # Attach our custom process_image method
+ self.ui.imageView.process_image = self.process_image
+ # Hook up to the newImageSignal so we can update
+ # our widgets after the new image is done
+ self.ui.imageView.newImageSignal.connect(self.show_blob)
+ # Store blob coordinate
+ self.blob = (0, 0)
+
+ def ui_filename(self):
+ # Point to our UI file
+ return "main.ui"
+
+ def ui_filepath(self):
+ # Return the full path to the UI file
+ return path.join(path.dirname(path.realpath(__file__)), self.ui_filename())
+
+ def show_blob(self, *args, **kwargs):
+ # If we have a blob, present the coordinates at label
+ if self.blob != (0, 0):
+ blob_txt = "Blob Found:"
+ blob_txt += " ({}, {})".format(self.blob[1], self.blob[0])
+ else:
+ # If no blob was found, present the "Not Found" message
+ blob_txt = "Blob Not Found"
+ # Update the label text
+ self.ui.lbl_blobs.setText(blob_txt)
+
+ def process_image(self, new_image):
+ # Consider the maximum as the Blob since we have only
+ # one blob.
+ self.blob = maximum_position(new_image)
+ # Send the original image data to the image widget
+ return new_image
diff --git a/examples/tutorial/main.ui b/examples/tutorial/main.ui
new file mode 100644
index 000000000..f34f42309
--- /dev/null
+++ b/examples/tutorial/main.ui
@@ -0,0 +1,321 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 724
+ 700
+
+
+
+
+ 0
+ 0
+
+
+
+ false
+
+
+ Beam Positioning
+
+
+ -
+
+
+ 0
+
+
-
+
+
+ QLabel {
+ qproperty-alignment: AlignCenter;
+ border: 1px solid #FF17365D;
+ border-top-left-radius: 15px;
+ border-top-right-radius: 15px;
+ background-color: #FF17365D;
+ padding: 5px 0px;
+ color: rgb(255, 255, 255);
+ max-height: 25px;
+ font-size: 14px;
+}
+
+
+
+ Beam Alignment
+
+
+
+ -
+
+
+
+ 600
+ 480
+
+
+
+
+
+
+
+ A PyQtGraph ImageView with support for Channels and more from PyDM.
+
+ If there is no :attr:`channelWidth` it is possible to define the width of
+ the image with the :attr:`width` property.
+
+ The :attr:`normalizeData` property defines if the colors of the images are
+ relative to the :attr:`colorMapMin` and :attr:`colorMapMax` property or to
+ the minimum and maximum values of the image.
+
+ Parameters
+ ----------
+ parent : QWidget
+ The parent widget for the Label
+ image_channel : str, optional
+ The channel to be used by the widget for the image data.
+ width_channel : str, optional
+ The channel to be used by the widget to receive the image width
+ information
+
+
+
+ 255.000000000000000
+
+
+ true
+
+
+ PyDMImageView::Clike
+
+
+ ca://IOC:Image
+
+
+ ca://IOC:ImageWidth
+
+
+ 30
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ -
+
+
+ 0
+
+
-
+
+
+
+
+
+
+ -
+
+
+ QLabel {
+ qproperty-alignment: AlignCenter;
+ border: 1px solid #FF17365D;
+ border-top-left-radius: 15px;
+ border-top-right-radius: 15px;
+ background-color: #FF17365D;
+ padding: 5px 0px;
+ color: rgb(255, 255, 255);
+ max-height: 25px;
+ font-size: 14px;
+}
+
+
+
+ Controls
+
+
+
+ -
+
+
+ QFrame#frame{
+ border: 1px solid #FF17365D;
+ border-bottom-left-radius: 15px;
+ border-bottom-right-radius: 15px;
+}
+
+
+ QFrame::StyledPanel
+
+
+ QFrame::Raised
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+
+ 700
+ 42
+
+
+
+
+ 16777215
+ 100
+
+
+
+
+
+
+
+ A QFrame capable of rendering a PyDM Display
+
+ Parameters
+ ----------
+ parent : QWidget
+ The parent widget for the Label
+
+
+
+
+ {"MOTOR":"IOC:m1"}
+
+
+ inline_motor.ui
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 700
+ 42
+
+
+
+
+ 16777215
+ 100
+
+
+
+
+
+
+
+ A QFrame capable of rendering a PyDM Display
+
+ Parameters
+ ----------
+ parent : QWidget
+ The parent widget for the Label
+
+
+
+
+ {"MOTOR":"IOC:m2"}
+
+
+ inline_motor.ui
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+ true
+
+
+
+
+
+
+ A QPushButton capable of opening a new Display at the same of at a
+ new window.
+
+ Parameters
+ ----------
+ init_channel : str, optional
+ The channel to be used by the widget.
+
+ filename : str, optional
+ The file to be opened
+
+
+
+ View All Motors
+
+
+ all_motors.py
+
+
+ false
+
+
+
+
+
+
+
+ PyDMEmbeddedDisplay
+ QFrame
+ pydm.widgets.embedded_display
+
+
+ PyDMImageView
+ QWidget
+
+
+
+ PyDMRelatedDisplayButton
+ QPushButton
+ pydm.widgets.related_display_button
+
+
+
+
+
diff --git a/examples/tutorial/motor_db.txt b/examples/tutorial/motor_db.txt
new file mode 100644
index 000000000..8cc77371f
--- /dev/null
+++ b/examples/tutorial/motor_db.txt
@@ -0,0 +1,8 @@
+IOC:m1
+IOC:m2
+IOC:m3
+IOC:m4
+IOC:m5
+IOC:m6
+IOC:m7
+IOC:m8
diff --git a/pydm/widgets/nt_table.py b/pydm/widgets/nt_table.py
index 783a296f3..15df9286d 100644
--- a/pydm/widgets/nt_table.py
+++ b/pydm/widgets/nt_table.py
@@ -175,11 +175,11 @@ class PyDMNTTable(QtWidgets.QWidget, PyDMWritableWidget):
and all the values stored by the keys will make up the values of the table.
Parameters
- ----------
- parent : QWidget, optional
- The parent widget for the PyDMNTTable
- init_channel : str, optional
- The channel to be used by the widget.
+ ----------
+ parent : QWidget, optional
+ The parent widget for the PyDMNTTable
+ init_channel : str, optional
+ The channel to be used by the widget.
"""
def __init__(self, parent=None, init_channel=None):
diff --git a/pydm/widgets/related_display_button.py b/pydm/widgets/related_display_button.py
index 21a272240..38fe6df74 100644
--- a/pydm/widgets/related_display_button.py
+++ b/pydm/widgets/related_display_button.py
@@ -269,6 +269,7 @@ def openInNewWindow(self, open_in_new: bool) -> None:
def passwordProtected(self) -> bool:
"""
Whether or not this button is password protected.
+
Returns
-------
bool
@@ -280,6 +281,7 @@ def passwordProtected(self) -> bool:
def passwordProtected(self, value: bool) -> None:
"""
Whether or not this button is password protected.
+
Parameters
----------
value : bool
@@ -293,8 +295,7 @@ def password(self) -> str:
Password to be encrypted using SHA256.
.. warning::
- To avoid issues exposing the password this method
- always returns an empty string.
+ To avoid issues exposing the password this method always returns an empty string.
Returns
-------
diff --git a/pydm/widgets/shell_command.py b/pydm/widgets/shell_command.py
index 23230c605..b50c6810e 100644
--- a/pydm/widgets/shell_command.py
+++ b/pydm/widgets/shell_command.py
@@ -330,6 +330,7 @@ def command(self, value: str) -> None:
def passwordProtected(self) -> bool:
"""
Whether or not this button is password protected.
+
Returns
-------
bool
@@ -341,6 +342,7 @@ def passwordProtected(self) -> bool:
def passwordProtected(self, value: bool) -> None:
"""
Whether or not this button is password protected.
+
Parameters
----------
value : bool
@@ -354,8 +356,7 @@ def password(self) -> str:
Password to be encrypted using SHA256.
.. warning::
- To avoid issues exposing the password this method
- always returns an empty string.
+ To avoid issues exposing the password this method always returns an empty string.
Returns
-------