diff --git a/.gitignore b/.gitignore
index 30511684f..d49200bbc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,3 +20,7 @@ build_log.txt
.logs/
.vscode/
20*.dat
+
+# bird droppings from IT
+.log
+.loglogin
diff --git a/.project b/.project
deleted file mode 100644
index 5334c032e..000000000
--- a/.project
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
- apstools
-
-
-
-
-
- org.python.pydev.PyDevBuilder
-
-
-
-
-
- org.python.pydev.pythonNature
-
-
diff --git a/.pydevproject b/.pydevproject
deleted file mode 100644
index de3b0a124..000000000
--- a/.pydevproject
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
- /${PROJECT_DIR_NAME}
-
- python interpreter
- bluesky
-
diff --git a/.travis.yml b/.travis.yml
index 5ef97e2b5..4c289ada5 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -14,6 +14,7 @@ python:
- "3.8"
# - "nightly"
+
# see https://github.com/BCDA-APS/apstools/issues/137#ref-commit-5c7baa2
# install:
# - pip install -r requirements.txt
@@ -24,7 +25,9 @@ before_script:
- bash miniconda.sh -b -p $HOME/miniconda
- export PATH=$HOME/miniconda/bin:$PATH
- export PY_ENV=travis-$TRAVIS_PYTHON_VERSION
- - conda create -y -n $PY_ENV -c apsu -c conda-forge -c nsls2forge -c defaults python=$TRAVIS_PYTHON_VERSION bluesky coverage databroker docopt epics-base numpy "ophyd>=1.4.0rc3" "pyepics>=3.4.1" pandas requests xlrd
+ - export MY_CONDA_CHANNELS="-c apsu -c conda-forge -c nsls2forge -c defaults -c aps-anl-tag"
+ - export MY_CONDA_PACKAGES="aps-dm-api bluesky>=1.6.2 coverage databroker>=1.0.6 docopt epics-base numpy ophyd>=1.5.1 pyepics>=3.4.2 pandas requests xlrd"
+ - conda create -y -n $PY_ENV $MY_CONDA_CHANNELS python=$TRAVIS_PYTHON_VERSION $MY_CONDA_PACKAGES
- source activate $PY_ENV
- which pip
- which python
diff --git a/apstools/beamtime/__init__.py b/apstools/beamtime/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/apstools/beamtime/apsbss.adl b/apstools/beamtime/apsbss.adl
new file mode 100644
index 000000000..e86f81951
--- /dev/null
+++ b/apstools/beamtime/apsbss.adl
@@ -0,0 +1,668 @@
+
+file {
+ name="/home/beams1/JEMIAN/Documents/projects/apstools/apstools/beamtime/apsbss.adl"
+ version=030111
+}
+display {
+ object {
+ x=122
+ y=83
+ width=400
+ height=366
+ }
+ clr=14
+ bclr=4
+ cmap=""
+ gridSpacing=5
+ gridOn=0
+ snapToGrid=0
+}
+"color map" {
+ ncolors=65
+ colors {
+ ffffff,
+ ececec,
+ dadada,
+ c8c8c8,
+ bbbbbb,
+ aeaeae,
+ 9e9e9e,
+ 919191,
+ 858585,
+ 787878,
+ 696969,
+ 5a5a5a,
+ 464646,
+ 2d2d2d,
+ 000000,
+ 00d800,
+ 1ebb00,
+ 339900,
+ 2d7f00,
+ 216c00,
+ fd0000,
+ de1309,
+ be190b,
+ a01207,
+ 820400,
+ 5893ff,
+ 597ee1,
+ 4b6ec7,
+ 3a5eab,
+ 27548d,
+ fbf34a,
+ f9da3c,
+ eeb62b,
+ e19015,
+ cd6100,
+ ffb0ff,
+ d67fe2,
+ ae4ebc,
+ 8b1a96,
+ 610a75,
+ a4aaff,
+ 8793e2,
+ 6a73c1,
+ 4d52a4,
+ 343386,
+ c7bb6d,
+ b79d5c,
+ a47e3c,
+ 7d5627,
+ 58340f,
+ 99ffff,
+ 73dfff,
+ 4ea5f9,
+ 2a63e4,
+ 0a00b8,
+ ebf1b5,
+ d4db9d,
+ bbc187,
+ a6a462,
+ 8b8239,
+ 73ff6b,
+ 52da3b,
+ 3cb420,
+ 289315,
+ 1a7309,
+ }
+}
+text {
+ object {
+ x=10
+ y=10
+ width=380
+ height=24
+ }
+ "basic attribute" {
+ clr=14
+ }
+ textix="APS ESAF & Proposal Info: $(P)"
+}
+composite {
+ object {
+ x=54
+ y=39
+ width=260
+ height=36
+ }
+ "composite name"=""
+ children {
+ text {
+ object {
+ x=54
+ y=39
+ width=120
+ height=14
+ }
+ "basic attribute" {
+ clr=14
+ }
+ textix="beam line name"
+ align="horiz. right"
+ }
+ "text entry" {
+ object {
+ x=179
+ y=39
+ width=135
+ height=16
+ }
+ control {
+ chan="$(P)proposal:beamline"
+ clr=14
+ bclr=50
+ }
+ limits {
+ }
+ }
+ text {
+ object {
+ x=54
+ y=59
+ width=120
+ height=14
+ }
+ "basic attribute" {
+ clr=14
+ }
+ textix="APS run cycle"
+ align="horiz. right"
+ }
+ "text entry" {
+ object {
+ x=179
+ y=59
+ width=135
+ height=16
+ }
+ control {
+ chan="$(P)esaf:cycle"
+ clr=14
+ bclr=50
+ }
+ limits {
+ }
+ }
+ }
+}
+"shell command" {
+ object {
+ x=19
+ y=80
+ width=200
+ height=16
+ }
+ command[0] {
+ label="update"
+ name="apsbss"
+ args="update $(P)"
+ }
+ clr=14
+ bclr=51
+ label=" get Proposal and ESAF info"
+}
+"shell command" {
+ object {
+ x=259
+ y=80
+ width=100
+ height=16
+ }
+ command[0] {
+ label="clear"
+ name="apsbss"
+ args="clear $(P)"
+ }
+ clr=14
+ bclr=51
+ label=" clear PVs"
+}
+composite {
+ object {
+ x=10
+ y=101
+ width=380
+ height=120
+ }
+ "composite name"=""
+ children {
+ text {
+ object {
+ x=17
+ y=106
+ width=120
+ height=16
+ }
+ "basic attribute" {
+ clr=14
+ }
+ textix="Proposal"
+ }
+ rectangle {
+ object {
+ x=10
+ y=101
+ width=380
+ height=120
+ }
+ "basic attribute" {
+ clr=14
+ fill="outline"
+ width=2
+ }
+ }
+ text {
+ object {
+ x=20
+ y=150
+ width=50
+ height=12
+ }
+ "basic attribute" {
+ clr=14
+ }
+ textix="mail in"
+ align="horiz. right"
+ }
+ text {
+ object {
+ x=20
+ y=167
+ width=50
+ height=12
+ }
+ "basic attribute" {
+ clr=14
+ }
+ textix="title"
+ align="horiz. right"
+ }
+ "text update" {
+ object {
+ x=77
+ y=167
+ width=303
+ height=12
+ }
+ monitor {
+ chan="$(P)proposal:title"
+ clr=14
+ bclr=55
+ }
+ format="string"
+ limits {
+ }
+ }
+ text {
+ object {
+ x=20
+ y=184
+ width=50
+ height=12
+ }
+ "basic attribute" {
+ clr=14
+ }
+ textix="users"
+ align="horiz. right"
+ }
+ "text update" {
+ object {
+ x=77
+ y=184
+ width=303
+ height=12
+ }
+ monitor {
+ chan="$(P)proposal:users"
+ clr=14
+ bclr=55
+ }
+ format="string"
+ limits {
+ }
+ }
+ "text update" {
+ object {
+ x=77
+ y=201
+ width=303
+ height=12
+ }
+ monitor {
+ chan="$(P)proposal:userBadges"
+ clr=14
+ bclr=55
+ }
+ format="string"
+ limits {
+ }
+ }
+ text {
+ object {
+ x=20
+ y=201
+ width=50
+ height=12
+ }
+ "basic attribute" {
+ clr=14
+ }
+ textix="badges"
+ align="horiz. right"
+ }
+ "choice button" {
+ object {
+ x=75
+ y=151
+ width=80
+ height=12
+ }
+ control {
+ chan="$(P)proposal:mailInFlag"
+ clr=14
+ bclr=55
+ }
+ stacking="column"
+ }
+ text {
+ object {
+ x=160
+ y=151
+ width=100
+ height=12
+ }
+ "basic attribute" {
+ clr=14
+ }
+ textix="proprietary"
+ align="horiz. right"
+ }
+ "choice button" {
+ object {
+ x=265
+ y=152
+ width=80
+ height=12
+ }
+ control {
+ chan="$(P)proposal:proprietaryFlag"
+ clr=14
+ bclr=55
+ }
+ stacking="column"
+ }
+ text {
+ object {
+ x=39
+ y=131
+ width=30
+ height=14
+ }
+ "basic attribute" {
+ clr=14
+ }
+ textix="ID"
+ align="horiz. right"
+ }
+ "text entry" {
+ object {
+ x=74
+ y=131
+ width=100
+ height=14
+ }
+ control {
+ chan="$(P)proposal:id"
+ clr=14
+ bclr=50
+ }
+ limits {
+ }
+ }
+ text {
+ object {
+ x=192
+ y=132
+ width=80
+ height=12
+ }
+ "basic attribute" {
+ clr=14
+ }
+ textix="submitted"
+ align="horiz. right"
+ }
+ "text entry" {
+ object {
+ x=277
+ y=132
+ width=100
+ height=12
+ }
+ control {
+ chan="$(P)proposal:submittedDate"
+ clr=14
+ bclr=55
+ }
+ limits {
+ }
+ }
+ }
+}
+composite {
+ object {
+ x=10
+ y=226
+ width=380
+ height=130
+ }
+ "composite name"=""
+ children {
+ text {
+ object {
+ x=17
+ y=231
+ width=120
+ height=16
+ }
+ "basic attribute" {
+ clr=14
+ }
+ textix="ESAF"
+ }
+ rectangle {
+ object {
+ x=10
+ y=226
+ width=380
+ height=130
+ }
+ "basic attribute" {
+ clr=14
+ fill="outline"
+ width=2
+ }
+ }
+ "text entry" {
+ object {
+ x=75
+ y=258
+ width=100
+ height=14
+ }
+ control {
+ chan="$(P)esaf:id"
+ clr=14
+ bclr=50
+ }
+ limits {
+ }
+ }
+ text {
+ object {
+ x=40
+ y=258
+ width=30
+ height=14
+ }
+ "basic attribute" {
+ clr=14
+ }
+ textix="ID"
+ align="horiz. right"
+ }
+ "text entry" {
+ object {
+ x=247
+ y=258
+ width=100
+ height=12
+ }
+ control {
+ chan="$(P)esaf:status"
+ clr=14
+ bclr=55
+ }
+ limits {
+ }
+ }
+ text {
+ object {
+ x=180
+ y=258
+ width=61
+ height=12
+ }
+ "basic attribute" {
+ clr=14
+ }
+ textix="status"
+ align="horiz. right"
+ }
+ text {
+ object {
+ x=20
+ y=277
+ width=50
+ height=12
+ }
+ "basic attribute" {
+ clr=14
+ }
+ textix="dates"
+ align="horiz. right"
+ }
+ "text entry" {
+ object {
+ x=75
+ y=277
+ width=101
+ height=12
+ }
+ control {
+ chan="$(P)esaf:startDate"
+ clr=14
+ bclr=55
+ }
+ limits {
+ }
+ }
+ text {
+ object {
+ x=181
+ y=277
+ width=20
+ height=12
+ }
+ "basic attribute" {
+ clr=14
+ }
+ textix="to"
+ align="horiz. right"
+ }
+ "text entry" {
+ object {
+ x=206
+ y=277
+ width=101
+ height=12
+ }
+ control {
+ chan="$(P)esaf:endDate"
+ clr=14
+ bclr=55
+ }
+ limits {
+ }
+ }
+ text {
+ object {
+ x=20
+ y=294
+ width=50
+ height=12
+ }
+ "basic attribute" {
+ clr=14
+ }
+ textix="title"
+ align="horiz. right"
+ }
+ "text update" {
+ object {
+ x=77
+ y=294
+ width=303
+ height=12
+ }
+ monitor {
+ chan="$(P)esaf:title"
+ clr=14
+ bclr=55
+ }
+ format="string"
+ limits {
+ }
+ }
+ text {
+ object {
+ x=20
+ y=311
+ width=50
+ height=12
+ }
+ "basic attribute" {
+ clr=14
+ }
+ textix="users"
+ align="horiz. right"
+ }
+ "text update" {
+ object {
+ x=77
+ y=311
+ width=303
+ height=12
+ }
+ monitor {
+ chan="$(P)esaf:users"
+ clr=14
+ bclr=55
+ }
+ format="string"
+ limits {
+ }
+ }
+ "text update" {
+ object {
+ x=77
+ y=328
+ width=303
+ height=12
+ }
+ monitor {
+ chan="$(P)esaf:userBadges"
+ clr=14
+ bclr=55
+ }
+ format="string"
+ limits {
+ }
+ }
+ text {
+ object {
+ x=20
+ y=328
+ width=50
+ height=12
+ }
+ "basic attribute" {
+ clr=14
+ }
+ textix="badges"
+ align="horiz. right"
+ }
+ }
+}
diff --git a/apstools/beamtime/apsbss.db b/apstools/beamtime/apsbss.db
new file mode 100644
index 000000000..d82feb753
--- /dev/null
+++ b/apstools/beamtime/apsbss.db
@@ -0,0 +1,345 @@
+#
+# file: apsbss.db
+# EPICS database for information from APS ESAF & Proposal databases
+#
+# BSS: Beamtime Scheduling System
+#
+# note: this file autogenerated by make_database.py
+# ./apsbss_makedb.py | tee apsbss.db
+#
+# Start the EPICS IOC with a command (from EPICS base):
+# softIoc -m P=ioc:bss: -d apsbss.db
+
+
+record(stringout, "$(P)esaf:cycle")
+
+record(waveform, "$(P)esaf:description") {
+ field(FTVL, "CHAR")
+ field(NELM, 4096)
+}
+
+record(stringout, "$(P)esaf:endDate")
+
+record(stringout, "$(P)esaf:id")
+
+record(stringout, "$(P)esaf:status")
+
+record(stringout, "$(P)esaf:sector")
+
+record(stringout, "$(P)esaf:startDate")
+
+record(waveform, "$(P)esaf:title") {
+ field(FTVL, "CHAR")
+ field(NELM, 1024)
+}
+
+record(waveform, "$(P)esaf:userBadges") {
+ field(FTVL, "CHAR")
+ field(NELM, 1024)
+}
+
+record(waveform, "$(P)esaf:users") {
+ field(FTVL, "CHAR")
+ field(NELM, 1024)
+}
+
+record(stringout, "$(P)esaf:user1:badgeNumber")
+
+record(stringout, "$(P)esaf:user1:email")
+
+record(stringout, "$(P)esaf:user1:firstName")
+
+record(stringout, "$(P)esaf:user1:lastName")
+
+record(stringout, "$(P)esaf:user2:badgeNumber")
+
+record(stringout, "$(P)esaf:user2:email")
+
+record(stringout, "$(P)esaf:user2:firstName")
+
+record(stringout, "$(P)esaf:user2:lastName")
+
+record(stringout, "$(P)esaf:user3:badgeNumber")
+
+record(stringout, "$(P)esaf:user3:email")
+
+record(stringout, "$(P)esaf:user3:firstName")
+
+record(stringout, "$(P)esaf:user3:lastName")
+
+record(stringout, "$(P)esaf:user4:badgeNumber")
+
+record(stringout, "$(P)esaf:user4:email")
+
+record(stringout, "$(P)esaf:user4:firstName")
+
+record(stringout, "$(P)esaf:user4:lastName")
+
+record(stringout, "$(P)esaf:user5:badgeNumber")
+
+record(stringout, "$(P)esaf:user5:email")
+
+record(stringout, "$(P)esaf:user5:firstName")
+
+record(stringout, "$(P)esaf:user5:lastName")
+
+record(stringout, "$(P)esaf:user6:badgeNumber")
+
+record(stringout, "$(P)esaf:user6:email")
+
+record(stringout, "$(P)esaf:user6:firstName")
+
+record(stringout, "$(P)esaf:user6:lastName")
+
+record(stringout, "$(P)esaf:user7:badgeNumber")
+
+record(stringout, "$(P)esaf:user7:email")
+
+record(stringout, "$(P)esaf:user7:firstName")
+
+record(stringout, "$(P)esaf:user7:lastName")
+
+record(stringout, "$(P)esaf:user8:badgeNumber")
+
+record(stringout, "$(P)esaf:user8:email")
+
+record(stringout, "$(P)esaf:user8:firstName")
+
+record(stringout, "$(P)esaf:user8:lastName")
+
+record(stringout, "$(P)esaf:user9:badgeNumber")
+
+record(stringout, "$(P)esaf:user9:email")
+
+record(stringout, "$(P)esaf:user9:firstName")
+
+record(stringout, "$(P)esaf:user9:lastName")
+
+record(stringout, "$(P)proposal:beamline")
+
+record(bo, "$(P)proposal:mailInFlag") {
+ field(ZNAM, "OFF")
+ field(ONAM, "ON")
+}
+
+record(stringout, "$(P)proposal:id")
+
+record(bo, "$(P)proposal:proprietaryFlag") {
+ field(ZNAM, "OFF")
+ field(ONAM, "ON")
+}
+
+record(stringout, "$(P)proposal:submittedDate")
+
+record(waveform, "$(P)proposal:title") {
+ field(FTVL, "CHAR")
+ field(NELM, 1024)
+}
+
+record(waveform, "$(P)proposal:userBadges") {
+ field(FTVL, "CHAR")
+ field(NELM, 1024)
+}
+
+record(waveform, "$(P)proposal:users") {
+ field(FTVL, "CHAR")
+ field(NELM, 1024)
+}
+
+record(stringout, "$(P)proposal:user1:badgeNumber")
+
+record(stringout, "$(P)proposal:user1:email")
+
+record(stringout, "$(P)proposal:user1:firstName")
+
+record(waveform, "$(P)proposal:user1:institution") {
+ field(FTVL, "CHAR")
+ field(NELM, 1024)
+}
+
+record(stringout, "$(P)proposal:user1:instId")
+
+record(stringout, "$(P)proposal:user1:lastName")
+
+record(bo, "$(P)proposal:user1:piFlag") {
+ field(ZNAM, "OFF")
+ field(ONAM, "ON")
+}
+
+record(stringout, "$(P)proposal:user1:userId")
+
+record(stringout, "$(P)proposal:user2:badgeNumber")
+
+record(stringout, "$(P)proposal:user2:email")
+
+record(stringout, "$(P)proposal:user2:firstName")
+
+record(waveform, "$(P)proposal:user2:institution") {
+ field(FTVL, "CHAR")
+ field(NELM, 1024)
+}
+
+record(stringout, "$(P)proposal:user2:instId")
+
+record(stringout, "$(P)proposal:user2:lastName")
+
+record(bo, "$(P)proposal:user2:piFlag") {
+ field(ZNAM, "OFF")
+ field(ONAM, "ON")
+}
+
+record(stringout, "$(P)proposal:user2:userId")
+
+record(stringout, "$(P)proposal:user3:badgeNumber")
+
+record(stringout, "$(P)proposal:user3:email")
+
+record(stringout, "$(P)proposal:user3:firstName")
+
+record(waveform, "$(P)proposal:user3:institution") {
+ field(FTVL, "CHAR")
+ field(NELM, 1024)
+}
+
+record(stringout, "$(P)proposal:user3:instId")
+
+record(stringout, "$(P)proposal:user3:lastName")
+
+record(bo, "$(P)proposal:user3:piFlag") {
+ field(ZNAM, "OFF")
+ field(ONAM, "ON")
+}
+
+record(stringout, "$(P)proposal:user3:userId")
+
+record(stringout, "$(P)proposal:user4:badgeNumber")
+
+record(stringout, "$(P)proposal:user4:email")
+
+record(stringout, "$(P)proposal:user4:firstName")
+
+record(waveform, "$(P)proposal:user4:institution") {
+ field(FTVL, "CHAR")
+ field(NELM, 1024)
+}
+
+record(stringout, "$(P)proposal:user4:instId")
+
+record(stringout, "$(P)proposal:user4:lastName")
+
+record(bo, "$(P)proposal:user4:piFlag") {
+ field(ZNAM, "OFF")
+ field(ONAM, "ON")
+}
+
+record(stringout, "$(P)proposal:user4:userId")
+
+record(stringout, "$(P)proposal:user5:badgeNumber")
+
+record(stringout, "$(P)proposal:user5:email")
+
+record(stringout, "$(P)proposal:user5:firstName")
+
+record(waveform, "$(P)proposal:user5:institution") {
+ field(FTVL, "CHAR")
+ field(NELM, 1024)
+}
+
+record(stringout, "$(P)proposal:user5:instId")
+
+record(stringout, "$(P)proposal:user5:lastName")
+
+record(bo, "$(P)proposal:user5:piFlag") {
+ field(ZNAM, "OFF")
+ field(ONAM, "ON")
+}
+
+record(stringout, "$(P)proposal:user5:userId")
+
+record(stringout, "$(P)proposal:user6:badgeNumber")
+
+record(stringout, "$(P)proposal:user6:email")
+
+record(stringout, "$(P)proposal:user6:firstName")
+
+record(waveform, "$(P)proposal:user6:institution") {
+ field(FTVL, "CHAR")
+ field(NELM, 1024)
+}
+
+record(stringout, "$(P)proposal:user6:instId")
+
+record(stringout, "$(P)proposal:user6:lastName")
+
+record(bo, "$(P)proposal:user6:piFlag") {
+ field(ZNAM, "OFF")
+ field(ONAM, "ON")
+}
+
+record(stringout, "$(P)proposal:user6:userId")
+
+record(stringout, "$(P)proposal:user7:badgeNumber")
+
+record(stringout, "$(P)proposal:user7:email")
+
+record(stringout, "$(P)proposal:user7:firstName")
+
+record(waveform, "$(P)proposal:user7:institution") {
+ field(FTVL, "CHAR")
+ field(NELM, 1024)
+}
+
+record(stringout, "$(P)proposal:user7:instId")
+
+record(stringout, "$(P)proposal:user7:lastName")
+
+record(bo, "$(P)proposal:user7:piFlag") {
+ field(ZNAM, "OFF")
+ field(ONAM, "ON")
+}
+
+record(stringout, "$(P)proposal:user7:userId")
+
+record(stringout, "$(P)proposal:user8:badgeNumber")
+
+record(stringout, "$(P)proposal:user8:email")
+
+record(stringout, "$(P)proposal:user8:firstName")
+
+record(waveform, "$(P)proposal:user8:institution") {
+ field(FTVL, "CHAR")
+ field(NELM, 1024)
+}
+
+record(stringout, "$(P)proposal:user8:instId")
+
+record(stringout, "$(P)proposal:user8:lastName")
+
+record(bo, "$(P)proposal:user8:piFlag") {
+ field(ZNAM, "OFF")
+ field(ONAM, "ON")
+}
+
+record(stringout, "$(P)proposal:user8:userId")
+
+record(stringout, "$(P)proposal:user9:badgeNumber")
+
+record(stringout, "$(P)proposal:user9:email")
+
+record(stringout, "$(P)proposal:user9:firstName")
+
+record(waveform, "$(P)proposal:user9:institution") {
+ field(FTVL, "CHAR")
+ field(NELM, 1024)
+}
+
+record(stringout, "$(P)proposal:user9:instId")
+
+record(stringout, "$(P)proposal:user9:lastName")
+
+record(bo, "$(P)proposal:user9:piFlag") {
+ field(ZNAM, "OFF")
+ field(ONAM, "ON")
+}
+
+record(stringout, "$(P)proposal:user9:userId")
diff --git a/apstools/beamtime/apsbss.py b/apstools/beamtime/apsbss.py
new file mode 100755
index 000000000..a263eebf9
--- /dev/null
+++ b/apstools/beamtime/apsbss.py
@@ -0,0 +1,710 @@
+#!/usr/bin/env python
+
+"""
+Retrieve specific records from the APS Proposal and ESAF databases.
+
+This code provides the command-line application: ``apsbss``
+
+.. note:: BSS: APS Beamline Scheduling System
+
+EXAMPLES::
+
+ apsbss current
+ apsbss esaf 226319
+ apsbss proposal 66083 2020-2 9-ID-B,C
+
+EPICS SUPPORT
+
+.. autosummary::
+
+ ~connect_epics
+ ~epicsClear
+ ~epicsSetup
+ ~epicsUpdate
+
+APS ESAF & PROPOSAL ACCESS
+
+.. autosummary::
+
+ ~getCurrentCycle
+ ~getCurrentEsafs
+ ~getCurrentInfo
+ ~getCurrentProposals
+ ~getEsaf
+ ~getProposal
+ ~iso2datetime
+ ~listAllBeamlines
+ ~listAllRuns
+ ~listRecentRuns
+ ~printColumns
+ ~trim
+
+APPLICATION
+
+.. autosummary::
+
+ ~cmd_current
+ ~cmd_esaf
+ ~cmd_proposal
+ ~get_options
+ ~main
+"""
+
+import datetime
+import dm # APS data management library
+import os
+import pyRestTable
+import sys
+import time
+import yaml
+
+from .apsbss_ophyd import EpicsBssDevice
+
+
+DM_APS_DB_WEB_SERVICE_URL = "https://xraydtn01.xray.aps.anl.gov:11236"
+CONNECT_TIMEOUT = 5
+POLL_INTERVAL = 0.01
+
+api_bss = dm.BssApsDbApi(DM_APS_DB_WEB_SERVICE_URL)
+api_esaf = dm.EsafApsDbApi(DM_APS_DB_WEB_SERVICE_URL)
+
+parser = None
+
+_cache_ = {}
+
+
+class EpicsNotConnected(Exception): ...
+
+class DmRecordNotFound(Exception): ...
+class EsafNotFound(DmRecordNotFound): ...
+class ProposalNotFound(DmRecordNotFound): ...
+
+
+def connect_epics(prefix):
+ """
+ Connect with the EPICS database instance.
+
+ PARAMETERS
+
+ prefix (str):
+ EPICS PV prefix
+ """
+ t0 = time.time()
+ t_timeout = t0 + CONNECT_TIMEOUT
+ bss = EpicsBssDevice(prefix, name="bss")
+ while not bss.connected and time.time() < t_timeout:
+ time.sleep(POLL_INTERVAL)
+ # printColumns(bss.read_attrs, width=40, numColumns=2)
+ if not bss.connected:
+ raise EpicsNotConnected(
+ f"Did not connect with EPICS {prefix} in {CONNECT_TIMEOUT}s")
+ t_connect = time.time() - t0
+ print(f"connected in {t_connect:.03f}s")
+ return bss
+
+
+def epicsClear(prefix):
+ """
+ Clear the EPICS database.
+ Connect with the EPICS database instance.
+
+ PARAMETERS
+
+ prefix (str):
+ EPICS PV prefix
+ """
+ print(f"clear EPICS {prefix}")
+ bss = connect_epics(prefix)
+
+ t0 = time.time()
+ bss.clear()
+ t_clear = time.time() - t0
+ print(f"cleared in {t_clear:.03f}s")
+
+
+def epicsUpdate(prefix):
+ """
+ Update EPICS database instance with current ESAF & proposal info.
+ Connect with the EPICS database instance.
+
+ PARAMETERS
+
+ prefix (str):
+ EPICS PV prefix
+ """
+ print(f"update EPICS {prefix}")
+ bss = connect_epics(prefix)
+
+ bss.clear()
+ cycle = bss.esaf.aps_cycle.get()
+ beamline = bss.proposal.beamline_name.get()
+ # sector = bss.esaf.sector.get()
+ esaf_id = bss.esaf.esaf_id.get()
+ proposal_id = bss.proposal.proposal_id.get()
+
+ if len(beamline) == 0:
+ raise ValueError(
+ f"must set beamline name in {bss.proposal.beamline_name.pvname}")
+ elif beamline not in listAllBeamlines():
+ raise ValueError(f"{beamline} is not known")
+ if len(cycle) == 0:
+ raise ValueError(
+ f"must set APS cycle name in {bss.esaf.aps_cycle.pvname}")
+ elif cycle not in listAllRuns():
+ raise ValueError(f"{cycle} is not known")
+
+ bss.clear()
+
+ if len(esaf_id) > 0:
+ esaf = getEsaf(esaf_id)
+ bss.esaf.description.put(esaf["description"])
+ bss.esaf.title.put(esaf["esafTitle"])
+ bss.esaf.esaf_status.put(esaf["esafStatus"])
+ bss.esaf.end_date.put(esaf["experimentEndDate"])
+ bss.esaf.start_date.put(esaf["experimentStartDate"])
+
+ bss.esaf.user_last_names.put(
+ ",".join([user["lastName"] for user in esaf["experimentUsers"]])
+ )
+ bss.esaf.user_badges.put(
+ ",".join([user["badge"] for user in esaf["experimentUsers"]])
+ )
+ for i, user in enumerate(esaf["experimentUsers"]):
+ obj = getattr(bss.esaf, f"user{i+1}")
+ obj.badge_number.put(user["badge"])
+ obj.email.put(user["email"])
+ obj.first_name.put(user["firstName"])
+ obj.last_name.put(user["lastName"])
+ if i == 9:
+ break
+
+ if len(proposal_id) > 0:
+ proposal = getProposal(proposal_id, cycle, beamline)
+ bss.proposal.mail_in_flag.put(proposal["mailInFlag"] in ("Y", "y"))
+ bss.proposal.proprietary_flag.put(proposal["proprietaryFlag"] in ("Y", "y"))
+ bss.proposal.submitted_date.put(proposal["submittedDate"])
+ bss.proposal.title.put(proposal["title"])
+
+ bss.proposal.user_last_names.put(
+ ",".join([user["lastName"] for user in proposal["experimenters"]])
+ )
+ bss.proposal.user_badges.put(
+ ",".join([user["badge"] for user in proposal["experimenters"]])
+ )
+ for i, user in enumerate(proposal["experimenters"]):
+ obj = getattr(bss.proposal, f"user{i+1}")
+ obj.badge_number.put(user["badge"])
+ obj.email.put(user["email"])
+ obj.first_name.put(user["firstName"])
+ obj.last_name.put(user["lastName"])
+ obj.institution.put(user["institution"])
+ obj.institution_id.put(str(user["instId"]))
+ obj.user_id.put(str(user["id"]))
+ obj.pi_flag.put(user.get("piFlag") in ("Y", "y"))
+ if i == 9:
+ break
+
+
+def epicsSetup(prefix, beamline, cycle=None):
+ """
+ Define the beamline name and APS cycle in the EPICS database.
+ Connect with the EPICS database instance.
+
+ PARAMETERS
+
+ prefix (str):
+ EPICS PV prefix
+ beamline (str):
+ Name of beam line (as defined by the BSS)
+ cycle (str):
+ Name of APS run cycle (as defined by the BSS).
+ optional: default is current APS run cycle name.
+ """
+ if beamline not in listAllBeamlines():
+ raise ValueError(f"{beamline} is not known")
+ if cycle is not None and cycle not in listAllRuns():
+ raise ValueError(f"{cycle} is not known")
+
+ bss = connect_epics(prefix)
+
+ cycle = cycle or getCurrentCycle()
+ sector = int(beamline.split("-")[0])
+ print(f"setup EPICS {prefix} {beamline} cycle={cycle} sector={sector}")
+
+ bss.clear()
+ bss.esaf.aps_cycle.put(cycle)
+ bss.proposal.beamline_name.put(beamline)
+ bss.esaf.sector.put(str(sector))
+
+
+def getCurrentCycle():
+ """Return the name of the current APS run cycle."""
+ return api_bss.getCurrentRun()["name"]
+
+
+def getCurrentEsafs(sector):
+ """
+ Return list of ESAFs for the current year.
+
+ PARAMETERS
+
+ sector (str or int):
+ Name of sector. If ``str``, must be in ``%02d`` format (``02``, not ``2``).
+ """
+ if isinstance(sector, int):
+ sector = f"{sector:02d}"
+ if len(sector) == 1:
+ sector = "0" + sector
+ tNow = datetime.datetime.now()
+ esafs = api_esaf.listEsafs(sector=sector, year=tNow.year)
+ results = []
+ for esaf in esafs:
+ if tNow < iso2datetime(esaf["experimentStartDate"]):
+ continue
+ if tNow > iso2datetime(esaf["experimentEndDate"]):
+ continue
+ results.append(esaf)
+ return results
+
+
+def getCurrentInfo(beamline):
+ """
+ From current year ESAFS, return list of ESAFs & proposals with same people.
+
+ PARAMETERS
+
+ beamline (str):
+ Name of beam line (as defined by the BSS).
+ """
+ sector = beamline.split("-")[0]
+ tNow = datetime.datetime.now()
+
+ matches = []
+ for esaf in api_esaf.listEsafs(sector=sector, year=tNow.year):
+ print(f"ESAF {esaf['esafId']}: {esaf['esafTitle']}")
+ esaf_badges = [user["badge"] for user in esaf["experimentUsers"]]
+ for run in listRecentRuns():
+ for proposal in api_bss.listProposals(beamlineName=beamline,
+ runName=run):
+ print(f"proposal {proposal['id']}: {proposal['title']}")
+ count = 0
+ for user in proposal["experimenters"]:
+ if user["badge"] in esaf_badges:
+ count += 1
+ if count > 0:
+ matches.append(
+ dict(
+ esaf=esaf,
+ proposal=proposal,
+ num_true=count,
+ num_esaf_badges=len(esaf_badges),
+ num_proposal_badges=len(proposal["experimenters"]),
+ )
+ )
+ return matches
+
+
+def getCurrentProposals(beamline):
+ """
+ Return a list of proposal ID numbers that are current.
+
+ PARAMETERS
+
+ beamline (str):
+ Name of beam line (as defined by the BSS).
+ """
+ proposals = []
+ for cycle in listRecentRuns():
+ for prop in api_bss.listProposals(beamlineName=beamline, runName=cycle):
+ prop = dict(prop)
+ prop["cycle"] = cycle
+ proposals.append(prop)
+ return proposals
+
+
+def getEsaf(esafId):
+ """
+ Return ESAF as a dictionary.
+
+ PARAMETERS
+
+ esafId (int):
+ ESAF number
+ """
+ try:
+ record = api_esaf.getEsaf(int(esafId))
+ except dm.ObjectNotFound:
+ raise EsafNotFound(esafId)
+ return dict(record.data)
+
+
+def getProposal(proposalId, cycle, beamline):
+ """
+ Return proposal as a dictionary.
+
+ PARAMETERS
+
+ proposalId (str):
+ Proposal identification number
+ cycle (str):
+ Name of APS run cycle (as defined by the BSS)
+ beamline (str):
+ Name of beam line (as defined by the BSS)
+ """
+ # avoid possible dm.DmException
+ if cycle not in listAllRuns():
+ raise DmRecordNotFound(f"cycle '{cycle}' not found")
+
+ if beamline not in listAllBeamlines():
+ raise DmRecordNotFound(f"beamline '{beamline}' not found")
+
+ try:
+ record = api_bss.getProposal(str(proposalId), cycle, beamline)
+ except dm.ObjectNotFound:
+ raise ProposalNotFound(
+ f"id={proposalId}"
+ f" cycle={cycle}"
+ f" beamline={beamline}"
+ )
+ return dict(record.data)
+
+
+def iso2datetime(isodate):
+ """
+ Convert a text ISO8601 date into a ``datetime`` object.
+
+ PARAMETERS
+
+ isodate (str):
+ Date and time in ISO8601 format. (e.g.: ``2020-07-01T12:34:56.789012``)
+ """
+ return datetime.datetime.fromisoformat(isodate)
+
+
+def listAllBeamlines():
+ """Return list (from ``dm``) of known beam line names."""
+ if "beamlines" not in _cache_:
+ _cache_["beamlines"] = [
+ entry["name"]
+ for entry in api_bss.listBeamlines()
+ ]
+ return _cache_["beamlines"]
+
+
+def listAllRuns():
+ """Return a list of all known cycles. Cache for repeated use."""
+ if "cycles" not in _cache_:
+ _cache_["cycles"] = sorted([
+ entry["name"]
+ for entry in api_bss.listRuns()
+ ])
+ return _cache_["cycles"]
+
+
+def listRecentRuns(quantity=6):
+ """
+ Return a list of the 6 most recent runs (2-year period).
+
+ PARAMETERS
+
+ quantity (int):
+ number of APS run cycles to include, optional (default: 6)
+ """
+ # 6 runs is the duration of a user proposal
+ tNow = datetime.datetime.now()
+ runs = [
+ run["name"]
+ for run in api_bss.listRuns()
+ if iso2datetime(run["startTime"]) <= tNow
+ ]
+ return sorted(runs, reverse=True)[:quantity]
+
+
+def printColumns(items, numColumns=5, width=10):
+ """
+ Print a list of ``items`` in column order.
+
+ PARAMETERS
+
+ items (list(str)):
+ List of items to report
+ numColumns (int):
+ number of columns, optional (default: 5)
+ width (int):
+ width of each column, optional (default: 10)
+ """
+ n = len(items)
+ rows = n // numColumns
+ if n % numColumns > 0:
+ rows += 1
+ for base in range(0, rows):
+ row = [
+ items[base+k*rows]
+ for k in range(numColumns)
+ if base+k*rows < n]
+ print("".join([f"{s:{width}s}" for s in row]))
+
+
+def trim(text, length=40):
+ """
+ Return a string that is no longer than ``length``.
+
+ If a string is longer than ``length``, it is shortened
+ to the ``length-3`` characters, then, ``...`` is appended.
+ For very short length, the string is shortened to ``length``
+ (and no ``...`` is appended).
+
+ PARAMETERS
+
+ text (str):
+ String, potentially longer than ``length``
+ length (int):
+ maximum length, optional (default: 40)
+ """
+ if length < 1:
+ raise ValueError(f"length must be positive, received {length}")
+ if length < 5:
+ text = text[:length]
+ elif len(text) > length:
+ text = text[:length-3] + "..."
+ return text
+
+
+def get_options():
+ """Handle command line arguments."""
+ global parser
+ import argparse
+ from apstools._version import get_versions
+
+ version = get_versions()['version']
+
+ parser = argparse.ArgumentParser(
+ prog=os.path.split(sys.argv[0])[-1],
+ description=__doc__.strip().splitlines()[0],
+ )
+
+ parser.add_argument('-v',
+ '--version',
+ action='version',
+ help='print version number and exit',
+ version=version)
+
+ subcommand = parser.add_subparsers(dest='subcommand', title='subcommand')
+
+ subcommand.add_parser('beamlines', help="print list of beamlines")
+
+ p_sub = subcommand.add_parser(
+ 'current',
+ help="print current ESAF(s) and proposal(s)")
+ p_sub.add_argument(
+ 'beamlineName',
+ type=str,
+ help="Beamline name")
+
+ p_sub = subcommand.add_parser('cycles', help="print APS run cycle names")
+ p_sub.add_argument(
+ '-f', '--full', action="store_true",
+ default=False,
+ help="full report including dates (default is compact)")
+ p_sub.add_argument(
+ '-a', '--ascending', action="store_false",
+ default=True,
+ help="full report by ascending names (default is descending)")
+
+ p_sub = subcommand.add_parser('esaf', help="print specific ESAF")
+ p_sub.add_argument('esafId', type=int, help="ESAF ID number")
+
+ p_sub = subcommand.add_parser('proposal', help="print specific proposal")
+ p_sub.add_argument('proposalId', type=str, help="proposal ID number")
+ p_sub.add_argument('cycle', type=str, help="APS run (cycle) name")
+ p_sub.add_argument('beamlineName', type=str, help="Beamline name")
+
+ p_sub = subcommand.add_parser('clear', help="EPICS PVs: clear")
+ p_sub.add_argument('prefix', type=str, help="EPICS PV prefix")
+
+ p_sub = subcommand.add_parser('setup', help="EPICS PVs: setup")
+ p_sub.add_argument('prefix', type=str, help="EPICS PV prefix")
+ p_sub.add_argument('beamlineName', type=str, help="Beamline name")
+ p_sub.add_argument('cycle', type=str, help="APS run (cycle) name")
+
+ p_sub = subcommand.add_parser('update', help="EPICS PVs: update from BSS")
+ p_sub.add_argument('prefix', type=str, help="EPICS PV prefix")
+
+ p_sub = subcommand.add_parser(
+ 'report',
+ help="EPICS PVs: report what is in the PVs")
+ p_sub.add_argument('prefix', type=str, help="EPICS PV prefix")
+
+ return parser.parse_args()
+
+def cmd_cycles(args):
+ """
+ Handle ``cycles`` command.
+
+ PARAMETERS
+
+ args (obj):
+ Object returned by ``argparse``
+ """
+ if args.full:
+ table = pyRestTable.Table()
+ table.labels = "cycle start end".split()
+
+ def sorter(entry):
+ return entry["startTime"]
+
+ for entry in sorted(api_bss.listRuns(),
+ key=sorter,
+ reverse=args.ascending):
+ table.addRow((
+ entry["name"],
+ entry["startTime"],
+ entry["endTime"], ))
+ print(table)
+ else:
+ printColumns(listAllRuns())
+
+def cmd_current(args):
+ """
+ Handle ``current`` command.
+
+ PARAMETERS
+
+ args (obj):
+ Object returned by ``argparse``
+ """
+ records = getCurrentProposals(args.beamlineName)
+ if len(records) == 0:
+ print(f"No current proposals for {args.beamlineName}")
+ else:
+ table = pyRestTable.Table()
+ table.labels = "id cycle date user(s) title".split()
+ for item in records:
+ users = trim(",".join([
+ user["lastName"]
+ for user in item["experimenters"]
+ ]), 20)
+ table.addRow((
+ item["id"],
+ item["cycle"],
+ item["submittedDate"],
+ users,
+ trim(item["title"]),))
+ print(f"Current Proposal(s) on {args.beamlineName}")
+ print()
+ print(table)
+
+ sector = args.beamlineName.split("-")[0]
+ records = getCurrentEsafs(sector)
+ if len(records) == 0:
+ print(f"No current ESAFs for sector {sector}")
+ else:
+ table = pyRestTable.Table()
+ table.labels = "id status start end user(s) title".split()
+ for item in records:
+ users = trim(
+ ",".join([
+ user["lastName"]
+ for user in item["experimentUsers"]
+ ]),
+ 20)
+ table.addRow((
+ item["esafId"],
+ item["esafStatus"],
+ item["experimentStartDate"].split()[0],
+ item["experimentEndDate"].split()[0],
+ users,
+ trim(item["esafTitle"], 40),
+ ))
+ print(f"Current ESAF(s) on sector {sector}")
+ print()
+ print(table)
+
+
+def cmd_esaf(args):
+ """
+ Handle ``esaf`` command.
+
+ PARAMETERS
+
+ args (obj):
+ Object returned by ``argparse``
+ """
+ try:
+ esaf = getEsaf(args.esafId)
+ print(yaml.dump(esaf))
+ except DmRecordNotFound as exc:
+ print(exc)
+ except dm.DmException as exc:
+ print(f"dm reported: {exc}")
+
+
+def cmd_proposal(args):
+ """
+ Handle ``proposal`` command.
+
+ PARAMETERS
+
+ args (obj):
+ Object returned by ``argparse``
+ """
+ try:
+ proposal = getProposal(args.proposalId, args.cycle, args.beamlineName)
+ print(yaml.dump(proposal))
+ except DmRecordNotFound as exc:
+ print(exc)
+ except dm.DmException as exc:
+ print(f"dm reported: {exc}")
+
+
+def cmd_report(args):
+ """
+ Handle ``report`` command.
+
+ PARAMETERS
+
+ args (obj):
+ Object returned by ``argparse``
+ """
+ from ..utils import object_explorer
+
+ bss = connect_epics(args.prefix)
+ object_explorer(bss)
+
+
+def main():
+ """Command-line interface for ``apsbss`` program."""
+ args = get_options()
+ if args.subcommand == "beamlines":
+ printColumns(listAllBeamlines(), numColumns=4, width=15)
+
+ elif args.subcommand == "clear":
+ epicsClear(args.prefix)
+
+ elif args.subcommand == "current":
+ cmd_current(args)
+
+ elif args.subcommand == "cycles":
+ cmd_cycles(args)
+
+ elif args.subcommand == "esaf":
+ cmd_esaf(args)
+
+ elif args.subcommand == "proposal":
+ cmd_proposal(args)
+
+ elif args.subcommand == "setup":
+ epicsSetup(args.prefix, args.beamlineName, args.cycle)
+
+ elif args.subcommand == "update":
+ epicsUpdate(args.prefix)
+
+ elif args.subcommand == "report":
+ cmd_report(args)
+
+ else:
+ parser.print_usage()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/apstools/beamtime/apsbss.ui b/apstools/beamtime/apsbss.ui
new file mode 100644
index 000000000..89b5fb501
--- /dev/null
+++ b/apstools/beamtime/apsbss.ui
@@ -0,0 +1,1730 @@
+
+
+MainWindow
+
+
+
+ 122
+ 83
+ 400
+ 366
+
+
+
+
+
+QWidget#centralWidget {background: rgba(187, 187, 187, 255);}
+QPushButton::menu-indicator {image: url(none.png); width: 0}
+
+
+
+
+
+
+ QFrame::NoFrame
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+ 0
+
+
+
+ APS ESAF & Proposal Info: $(P)
+
+
+ ESimpleLabel::WidthAndHeight
+
+
+
+ 10
+ 10
+ 380
+ 24
+
+
+
+ Qt::AlignAbsolute|Qt::AlignLeft|Qt::AlignVCenter
+
+
+
+
+
+ 54
+ 39
+ 262
+ 38
+
+
+
+
+ QFrame::NoFrame
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+ 0
+
+
+
+ beam line name
+
+
+ ESimpleLabel::WidthAndHeight
+
+
+ Qt::AlignAbsolute|Qt::AlignRight|Qt::AlignVCenter
+
+
+
+ 0
+ 0
+ 120
+ 14
+
+
+
+
+
+
+ 125
+ 0
+ 135
+ 16
+
+
+
+ caLineEdit::WidthAndHeight
+
+
+ $(P)proposal:beamline
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 153
+ 255
+ 255
+
+
+
+ caLineEdit::Channel
+
+
+ caLineEdit::Channel
+
+
+ caLineEdit::Channel
+
+
+ 0.0
+
+
+ 1.0
+
+
+ caLineEdit::Static
+
+
+ decimal
+
+
+
+
+ QFrame::NoFrame
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+ 0
+
+
+
+ APS run cycle
+
+
+ ESimpleLabel::WidthAndHeight
+
+
+ Qt::AlignAbsolute|Qt::AlignRight|Qt::AlignVCenter
+
+
+
+ 0
+ 20
+ 120
+ 14
+
+
+
+
+
+
+ 125
+ 20
+ 135
+ 16
+
+
+
+ caLineEdit::WidthAndHeight
+
+
+ $(P)esaf:cycle
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 153
+ 255
+ 255
+
+
+
+ caLineEdit::Channel
+
+
+ caLineEdit::Channel
+
+
+ caLineEdit::Channel
+
+
+ 0.0
+
+
+ 1.0
+
+
+ caLineEdit::Static
+
+
+ decimal
+
+
+
+
+
+
+ 19
+ 80
+ 200
+ 16
+
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 115
+ 223
+ 255
+
+
+
+ get Proposal and ESAF info
+
+
+ update
+
+
+ apsbss
+
+
+ update $(P)
+
+
+
+
+
+ 259
+ 80
+ 100
+ 16
+
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 115
+ 223
+ 255
+
+
+
+ clear PVs
+
+
+ clear
+
+
+ apsbss
+
+
+ clear $(P)
+
+
+
+
+
+ 10
+ 101
+ 382
+ 122
+
+
+
+
+ QFrame::NoFrame
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+ 0
+
+
+
+ Proposal
+
+
+ ESimpleLabel::WidthAndHeight
+
+
+
+ 7
+ 5
+ 120
+ 16
+
+
+
+ Qt::AlignAbsolute|Qt::AlignLeft|Qt::AlignVCenter
+
+
+
+
+ caGraphics::Rectangle
+
+
+
+ 0
+ 0
+ 380
+ 120
+
+
+
+ 2
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+ 0
+
+
+
+ Solid
+
+
+
+
+ QFrame::NoFrame
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+ 0
+
+
+
+ mail in
+
+
+ ESimpleLabel::WidthAndHeight
+
+
+ Qt::AlignAbsolute|Qt::AlignRight|Qt::AlignVCenter
+
+
+
+ 10
+ 49
+ 50
+ 12
+
+
+
+
+
+ QFrame::NoFrame
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+ 0
+
+
+
+ title
+
+
+ ESimpleLabel::WidthAndHeight
+
+
+ Qt::AlignAbsolute|Qt::AlignRight|Qt::AlignVCenter
+
+
+
+ 10
+ 66
+ 50
+ 12
+
+
+
+
+
+
+ 67
+ 66
+ 303
+ 12
+
+
+
+ caLineEdit::WidthAndHeight
+
+
+ $(P)proposal:title
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 235
+ 241
+ 181
+
+
+
+ caLineEdit::Channel
+
+
+ caLineEdit::Channel
+
+
+ caLineEdit::Channel
+
+
+ 0.0
+
+
+ 1.0
+
+
+ Qt::AlignAbsolute|Qt::AlignLeft|Qt::AlignVCenter
+
+
+ string
+
+
+ caLineEdit::Static
+
+
+
+
+ QFrame::NoFrame
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+ 0
+
+
+
+ users
+
+
+ ESimpleLabel::WidthAndHeight
+
+
+ Qt::AlignAbsolute|Qt::AlignRight|Qt::AlignVCenter
+
+
+
+ 10
+ 83
+ 50
+ 12
+
+
+
+
+
+
+ 67
+ 83
+ 303
+ 12
+
+
+
+ caLineEdit::WidthAndHeight
+
+
+ $(P)proposal:users
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 235
+ 241
+ 181
+
+
+
+ caLineEdit::Channel
+
+
+ caLineEdit::Channel
+
+
+ caLineEdit::Channel
+
+
+ 0.0
+
+
+ 1.0
+
+
+ Qt::AlignAbsolute|Qt::AlignLeft|Qt::AlignVCenter
+
+
+ string
+
+
+ caLineEdit::Static
+
+
+
+
+
+ 67
+ 100
+ 303
+ 12
+
+
+
+ caLineEdit::WidthAndHeight
+
+
+ $(P)proposal:userBadges
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 235
+ 241
+ 181
+
+
+
+ caLineEdit::Channel
+
+
+ caLineEdit::Channel
+
+
+ caLineEdit::Channel
+
+
+ 0.0
+
+
+ 1.0
+
+
+ Qt::AlignAbsolute|Qt::AlignLeft|Qt::AlignVCenter
+
+
+ string
+
+
+ caLineEdit::Static
+
+
+
+
+ QFrame::NoFrame
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+ 0
+
+
+
+ badges
+
+
+ ESimpleLabel::WidthAndHeight
+
+
+ Qt::AlignAbsolute|Qt::AlignRight|Qt::AlignVCenter
+
+
+
+ 10
+ 100
+ 50
+ 12
+
+
+
+
+
+
+ 65
+ 50
+ 80
+ 12
+
+
+
+ $(P)proposal:mailInFlag
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 235
+ 241
+ 181
+
+
+
+ Column
+
+
+ caChoice::Static
+
+
+
+
+ QFrame::NoFrame
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+ 0
+
+
+
+ proprietary
+
+
+ ESimpleLabel::WidthAndHeight
+
+
+ Qt::AlignAbsolute|Qt::AlignRight|Qt::AlignVCenter
+
+
+
+ 150
+ 50
+ 100
+ 12
+
+
+
+
+
+
+ 255
+ 51
+ 80
+ 12
+
+
+
+ $(P)proposal:proprietaryFlag
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 235
+ 241
+ 181
+
+
+
+ Column
+
+
+ caChoice::Static
+
+
+
+
+ QFrame::NoFrame
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+ 0
+
+
+
+ ID
+
+
+ ESimpleLabel::WidthAndHeight
+
+
+ Qt::AlignAbsolute|Qt::AlignRight|Qt::AlignVCenter
+
+
+
+ 29
+ 30
+ 30
+ 14
+
+
+
+
+
+
+ 64
+ 30
+ 100
+ 14
+
+
+
+ caLineEdit::WidthAndHeight
+
+
+ $(P)proposal:id
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 153
+ 255
+ 255
+
+
+
+ caLineEdit::Channel
+
+
+ caLineEdit::Channel
+
+
+ caLineEdit::Channel
+
+
+ 0.0
+
+
+ 1.0
+
+
+ caLineEdit::Static
+
+
+ decimal
+
+
+
+
+ QFrame::NoFrame
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+ 0
+
+
+
+ submitted
+
+
+ ESimpleLabel::WidthAndHeight
+
+
+ Qt::AlignAbsolute|Qt::AlignRight|Qt::AlignVCenter
+
+
+
+ 182
+ 31
+ 80
+ 12
+
+
+
+
+
+
+ 267
+ 31
+ 100
+ 12
+
+
+
+ caLineEdit::WidthAndHeight
+
+
+ $(P)proposal:submittedDate
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 235
+ 241
+ 181
+
+
+
+ caLineEdit::Channel
+
+
+ caLineEdit::Channel
+
+
+ caLineEdit::Channel
+
+
+ 0.0
+
+
+ 1.0
+
+
+ caLineEdit::Static
+
+
+ decimal
+
+
+
+
+
+
+ 10
+ 226
+ 382
+ 132
+
+
+
+
+ QFrame::NoFrame
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+ 0
+
+
+
+ ESAF
+
+
+ ESimpleLabel::WidthAndHeight
+
+
+
+ 7
+ 5
+ 120
+ 16
+
+
+
+ Qt::AlignAbsolute|Qt::AlignLeft|Qt::AlignVCenter
+
+
+
+
+ caGraphics::Rectangle
+
+
+
+ 0
+ 0
+ 380
+ 130
+
+
+
+ 2
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+ 0
+
+
+
+ Solid
+
+
+
+
+
+ 65
+ 32
+ 100
+ 14
+
+
+
+ caLineEdit::WidthAndHeight
+
+
+ $(P)esaf:id
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 153
+ 255
+ 255
+
+
+
+ caLineEdit::Channel
+
+
+ caLineEdit::Channel
+
+
+ caLineEdit::Channel
+
+
+ 0.0
+
+
+ 1.0
+
+
+ caLineEdit::Static
+
+
+ decimal
+
+
+
+
+ QFrame::NoFrame
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+ 0
+
+
+
+ ID
+
+
+ ESimpleLabel::WidthAndHeight
+
+
+ Qt::AlignAbsolute|Qt::AlignRight|Qt::AlignVCenter
+
+
+
+ 30
+ 32
+ 30
+ 14
+
+
+
+
+
+
+ 237
+ 32
+ 100
+ 12
+
+
+
+ caLineEdit::WidthAndHeight
+
+
+ $(P)esaf:status
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 235
+ 241
+ 181
+
+
+
+ caLineEdit::Channel
+
+
+ caLineEdit::Channel
+
+
+ caLineEdit::Channel
+
+
+ 0.0
+
+
+ 1.0
+
+
+ caLineEdit::Static
+
+
+ decimal
+
+
+
+
+ QFrame::NoFrame
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+ 0
+
+
+
+ status
+
+
+ ESimpleLabel::WidthAndHeight
+
+
+ Qt::AlignAbsolute|Qt::AlignRight|Qt::AlignVCenter
+
+
+
+ 170
+ 32
+ 61
+ 12
+
+
+
+
+
+ QFrame::NoFrame
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+ 0
+
+
+
+ dates
+
+
+ ESimpleLabel::WidthAndHeight
+
+
+ Qt::AlignAbsolute|Qt::AlignRight|Qt::AlignVCenter
+
+
+
+ 10
+ 51
+ 50
+ 12
+
+
+
+
+
+
+ 65
+ 51
+ 101
+ 12
+
+
+
+ caLineEdit::WidthAndHeight
+
+
+ $(P)esaf:startDate
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 235
+ 241
+ 181
+
+
+
+ caLineEdit::Channel
+
+
+ caLineEdit::Channel
+
+
+ caLineEdit::Channel
+
+
+ 0.0
+
+
+ 1.0
+
+
+ caLineEdit::Static
+
+
+ decimal
+
+
+
+
+ QFrame::NoFrame
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+ 0
+
+
+
+ to
+
+
+ ESimpleLabel::WidthAndHeight
+
+
+ Qt::AlignAbsolute|Qt::AlignRight|Qt::AlignVCenter
+
+
+
+ 171
+ 51
+ 20
+ 12
+
+
+
+
+
+
+ 196
+ 51
+ 101
+ 12
+
+
+
+ caLineEdit::WidthAndHeight
+
+
+ $(P)esaf:endDate
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 235
+ 241
+ 181
+
+
+
+ caLineEdit::Channel
+
+
+ caLineEdit::Channel
+
+
+ caLineEdit::Channel
+
+
+ 0.0
+
+
+ 1.0
+
+
+ caLineEdit::Static
+
+
+ decimal
+
+
+
+
+ QFrame::NoFrame
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+ 0
+
+
+
+ title
+
+
+ ESimpleLabel::WidthAndHeight
+
+
+ Qt::AlignAbsolute|Qt::AlignRight|Qt::AlignVCenter
+
+
+
+ 10
+ 68
+ 50
+ 12
+
+
+
+
+
+
+ 67
+ 68
+ 303
+ 12
+
+
+
+ caLineEdit::WidthAndHeight
+
+
+ $(P)esaf:title
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 235
+ 241
+ 181
+
+
+
+ caLineEdit::Channel
+
+
+ caLineEdit::Channel
+
+
+ caLineEdit::Channel
+
+
+ 0.0
+
+
+ 1.0
+
+
+ Qt::AlignAbsolute|Qt::AlignLeft|Qt::AlignVCenter
+
+
+ string
+
+
+ caLineEdit::Static
+
+
+
+
+ QFrame::NoFrame
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+ 0
+
+
+
+ users
+
+
+ ESimpleLabel::WidthAndHeight
+
+
+ Qt::AlignAbsolute|Qt::AlignRight|Qt::AlignVCenter
+
+
+
+ 10
+ 85
+ 50
+ 12
+
+
+
+
+
+
+ 67
+ 85
+ 303
+ 12
+
+
+
+ caLineEdit::WidthAndHeight
+
+
+ $(P)esaf:users
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 235
+ 241
+ 181
+
+
+
+ caLineEdit::Channel
+
+
+ caLineEdit::Channel
+
+
+ caLineEdit::Channel
+
+
+ 0.0
+
+
+ 1.0
+
+
+ Qt::AlignAbsolute|Qt::AlignLeft|Qt::AlignVCenter
+
+
+ string
+
+
+ caLineEdit::Static
+
+
+
+
+
+ 67
+ 102
+ 303
+ 12
+
+
+
+ caLineEdit::WidthAndHeight
+
+
+ $(P)esaf:userBadges
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 235
+ 241
+ 181
+
+
+
+ caLineEdit::Channel
+
+
+ caLineEdit::Channel
+
+
+ caLineEdit::Channel
+
+
+ 0.0
+
+
+ 1.0
+
+
+ Qt::AlignAbsolute|Qt::AlignLeft|Qt::AlignVCenter
+
+
+ string
+
+
+ caLineEdit::Static
+
+
+
+
+ QFrame::NoFrame
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+ 0
+
+
+
+ badges
+
+
+ ESimpleLabel::WidthAndHeight
+
+
+ Qt::AlignAbsolute|Qt::AlignRight|Qt::AlignVCenter
+
+
+
+ 10
+ 102
+ 50
+ 12
+
+
+
+
+ caLabel_0
+ caLabel_1
+ caLabel_2
+ caFrame_0
+ caLabel_3
+ caRectangle_0
+ caLabel_4
+ caLabel_5
+ caLabel_6
+ caLabel_7
+ caLabel_8
+ caLabel_9
+ caLabel_10
+ caFrame_1
+ caLabel_11
+ caRectangle_1
+ caLabel_12
+ caLabel_13
+ caLabel_14
+ caLabel_15
+ caLabel_16
+ caLabel_17
+ caLabel_18
+ caFrame_2
+ caTextEntry_0
+ caTextEntry_1
+ caShellCommand_0
+ caShellCommand_1
+ caLineEdit_0
+ caLineEdit_1
+ caLineEdit_2
+ caChoice_0
+ caChoice_1
+ caTextEntry_2
+ caTextEntry_3
+ caTextEntry_4
+ caTextEntry_5
+ caTextEntry_6
+ caTextEntry_7
+ caLineEdit_3
+ caLineEdit_4
+ caLineEdit_5
+
+
+
\ No newline at end of file
diff --git a/apstools/beamtime/apsbss_ioc.sh b/apstools/beamtime/apsbss_ioc.sh
new file mode 100755
index 000000000..96403e467
--- /dev/null
+++ b/apstools/beamtime/apsbss_ioc.sh
@@ -0,0 +1,178 @@
+#!/bin/bash
+
+# manage the IOC for apsbss
+
+#--------------------
+# change the program defaults here
+DEFAULT_SESSION_NAME=apsbss
+DEFAULT_IOC_PREFIX=ioc:bss:
+#--------------------
+
+SHELL_SCRIPT_NAME=${BASH_SOURCE:-${0}}
+SELECTION=${1:-usage}
+SESSION_NAME=${2:-"${DEFAULT_SESSION_NAME}"}
+IOC_PREFIX=${3:-"${DEFAULT_IOC_PREFIX}"}
+
+IOC_BINARY=softIoc
+EPICS_DATABASE=apsbss.db
+START_IOC_COMMAND="${IOC_BINARY} -m P=${IOC_PREFIX} -d ${EPICS_DATABASE}"
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+if [ -z "$IOC_STARTUP_DIR" ] ; then
+ # If no startup dir is specified, use the directory with this script
+ IOC_STARTUP_DIR=$(dirname "${SHELL_SCRIPT_NAME}")
+fi
+
+# echo "SESSION_NAME = ${SESSION_NAME}"
+# echo "IOC_PREFIX = ${IOC_PREFIX}"
+# echo "START_IOC_COMMAND = ${START_IOC_COMMAND}"
+# echo "SHELL_SCRIPT_NAME = ${SHELL_SCRIPT_NAME}"
+# echo "IOC_STARTUP_DIR = ${IOC_STARTUP_DIR}"
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+function checkpid() {
+ MY_UID=$(id -u)
+ # # The '\$' is needed in the pgrep pattern to select vm7, but not vm7.sh
+ IOC_PID=$(pgrep "${IOC_BINARY}"\$ -u "${MY_UID}")
+ # #!echo "IOC_PID=${IOC_PID}"
+
+ if [ "${IOC_PID}" != "" ] ; then
+ # Assume the IOC is down until proven otherwise
+ IOC_DOWN=1
+ SCREEN_PID=""
+
+ # At least one instance of the IOC binary is running;
+ # Find the binary that is associated with this script/IOC
+ for pid in ${IOC_PID}; do
+ # compare directories
+ BIN_CWD=$(readlink "/proc/${pid}/cwd")
+ IOC_CWD=$(readlink -f "${IOC_STARTUP_DIR}")
+
+ if [ "$BIN_CWD" = "$IOC_CWD" ] ; then
+ # The IOC is running;
+ # the process with PID=$pid is the
+ # IOC that was run from $IOC_STARTUP_DIR
+ P_PID=$(ps -p "${pid}" -o ppid=)
+ # strip leading (and trailing) whitespace
+ arr=($P_PID)
+ P_PID=${arr[0]}
+ SCREEN_SESSION="${P_PID}.${SESSION_NAME}"
+ SCREEN_MATCH=$(screen -ls "${SCREEN_SESSION}" | grep "${SESSION_NAME}")
+ if [ "${SCREEN_MATCH}" != "" ] ; then
+ # IOC is running in screen
+ IOC_DOWN=0
+ IOC_PID=${pid}
+ SCREEN_PID=${P_PID}
+ break
+ fi
+ fi
+ done
+ else
+ # IOC is not running
+ IOC_DOWN=1
+ fi
+
+ return ${IOC_DOWN}
+}
+
+function console () {
+ if checkpid; then
+ echo "Connecting to ${SCREEN_SESSION}'s screen session"
+ # The -r flag will only connect if no one is attached to the session
+ #!screen -r "${SESSION_NAME}"
+ # The -x flag will connect even if someone is attached to the session
+ screen -x "${SCREEN_SESSION}"
+ else
+ echo "${SCREEN_NAME} is not running"
+ fi
+}
+
+function exit_if_running() {
+ # ensure that multiple, simultaneous IOCs are not started by this user ID
+ MY_UID=$(id -u)
+ IOC_PID=$(pgrep "${SESSION_NAME}"\$ -u "${MY_UID}")
+
+ if [ "" != "${IOC_PID}" ] ; then
+ echo "${SESSION_NAME} IOC is already running (PID=${IOC_PID}), won't start a new one"
+ exit 1
+ fi
+}
+
+function restart() {
+ stop
+ start
+}
+
+function run_ioc() {
+ exit_if_running
+ ${START_IOC_COMMAND}
+}
+
+function screenpid() {
+ if [ -z "${SCREEN_PID}" ] ; then
+ echo
+ else
+ echo " in a screen session (pid=${SCREEN_PID})"
+ fi
+}
+
+function start() {
+ if checkpid; then
+ echo -n "${SCREEN_SESSION} is already running (pid=${IOC_PID})"
+ screenpid
+ else
+ echo "Starting ${SESSION_NAME} with IOC prefix ${IOC_PREFIX}"
+ cd "${IOC_STARTUP_DIR}"
+ # Run SESSION_NAME inside a screen session
+ CMD="screen -dm -S ${SESSION_NAME} -h 5000 ${START_IOC_COMMAND}"
+ $CMD
+ fi
+}
+
+function status() {
+ if checkpid; then
+ echo -n "${SCREEN_SESSION} is running (pid=${IOC_PID})"
+ screenpid
+ else
+ echo "${SESSION_NAME} is not running"
+ fi
+}
+
+function stop() {
+ if checkpid; then
+ echo "Stopping ${SCREEN_SESSION} (pid=${IOC_PID})"
+ kill "${IOC_PID}"
+ else
+ echo "${SESSION_NAME} is not running"
+ fi
+}
+
+function usage() {
+ echo "Usage: $(basename "${SHELL_SCRIPT_NAME}") {start|stop|restart|status|console|run} [NAME [PREFIX]]"
+ echo ""
+ echo " COMMANDS"
+ echo " console attach to IOC console if IOC is running in screen"
+ echo " restart restart IOC"
+ echo " run run IOC in console (not screen)"
+ echo " start start IOC"
+ echo " status report if IOC is running"
+ echo " stop stop IOC"
+ echo ""
+ echo " OPTIONAL TERMS"
+ echo " NAME name of IOC session (default: ${DEFAULT_SESSION_NAME})"
+ echo " PREFIX IOC prefix (default: ${DEFAULT_IOC_PREFIX})"
+}
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+case ${SELECTION} in
+ start) start ;;
+ stop | kill) stop ;;
+ restart) restart ;;
+ status) status ;;
+ console) console ;;
+ run) run_ioc ;;
+ *) usage ;;
+esac
diff --git a/apstools/beamtime/apsbss_makedb.py b/apstools/beamtime/apsbss_makedb.py
new file mode 100755
index 000000000..d470b6aa8
--- /dev/null
+++ b/apstools/beamtime/apsbss_makedb.py
@@ -0,0 +1,183 @@
+#!/usr/bin/env python
+
+"""
+Create the EPICS database
+
+This is just a service program to make the ``.db`` file.
+
+.. autosummary::
+
+ ~main
+"""
+
+#name RTYP length
+
+raw_data = """
+esaf:cycle stringout
+esaf:description waveform 4096
+esaf:endDate stringout
+esaf:id stringout
+esaf:status stringout
+esaf:sector stringout
+esaf:startDate stringout
+esaf:title waveform 1024
+esaf:userBadges waveform 1024
+esaf:users waveform 1024
+esaf:user1:badgeNumber stringout
+esaf:user1:email stringout
+esaf:user1:firstName stringout
+esaf:user1:lastName stringout
+esaf:user2:badgeNumber stringout
+esaf:user2:email stringout
+esaf:user2:firstName stringout
+esaf:user2:lastName stringout
+esaf:user3:badgeNumber stringout
+esaf:user3:email stringout
+esaf:user3:firstName stringout
+esaf:user3:lastName stringout
+esaf:user4:badgeNumber stringout
+esaf:user4:email stringout
+esaf:user4:firstName stringout
+esaf:user4:lastName stringout
+esaf:user5:badgeNumber stringout
+esaf:user5:email stringout
+esaf:user5:firstName stringout
+esaf:user5:lastName stringout
+esaf:user6:badgeNumber stringout
+esaf:user6:email stringout
+esaf:user6:firstName stringout
+esaf:user6:lastName stringout
+esaf:user7:badgeNumber stringout
+esaf:user7:email stringout
+esaf:user7:firstName stringout
+esaf:user7:lastName stringout
+esaf:user8:badgeNumber stringout
+esaf:user8:email stringout
+esaf:user8:firstName stringout
+esaf:user8:lastName stringout
+esaf:user9:badgeNumber stringout
+esaf:user9:email stringout
+esaf:user9:firstName stringout
+esaf:user9:lastName stringout
+proposal:beamline stringout
+proposal:mailInFlag bo
+proposal:id stringout
+proposal:proprietaryFlag bo
+proposal:submittedDate stringout
+proposal:title waveform 1024
+proposal:userBadges waveform 1024
+proposal:users waveform 1024
+proposal:user1:badgeNumber stringout
+proposal:user1:email stringout
+proposal:user1:firstName stringout
+proposal:user1:institution waveform 1024
+proposal:user1:instId stringout
+proposal:user1:lastName stringout
+proposal:user1:piFlag bo
+proposal:user1:userId stringout
+proposal:user2:badgeNumber stringout
+proposal:user2:email stringout
+proposal:user2:firstName stringout
+proposal:user2:institution waveform 1024
+proposal:user2:instId stringout
+proposal:user2:lastName stringout
+proposal:user2:piFlag bo
+proposal:user2:userId stringout
+proposal:user3:badgeNumber stringout
+proposal:user3:email stringout
+proposal:user3:firstName stringout
+proposal:user3:institution waveform 1024
+proposal:user3:instId stringout
+proposal:user3:lastName stringout
+proposal:user3:piFlag bo
+proposal:user3:userId stringout
+proposal:user4:badgeNumber stringout
+proposal:user4:email stringout
+proposal:user4:firstName stringout
+proposal:user4:institution waveform 1024
+proposal:user4:instId stringout
+proposal:user4:lastName stringout
+proposal:user4:piFlag bo
+proposal:user4:userId stringout
+proposal:user5:badgeNumber stringout
+proposal:user5:email stringout
+proposal:user5:firstName stringout
+proposal:user5:institution waveform 1024
+proposal:user5:instId stringout
+proposal:user5:lastName stringout
+proposal:user5:piFlag bo
+proposal:user5:userId stringout
+proposal:user6:badgeNumber stringout
+proposal:user6:email stringout
+proposal:user6:firstName stringout
+proposal:user6:institution waveform 1024
+proposal:user6:instId stringout
+proposal:user6:lastName stringout
+proposal:user6:piFlag bo
+proposal:user6:userId stringout
+proposal:user7:badgeNumber stringout
+proposal:user7:email stringout
+proposal:user7:firstName stringout
+proposal:user7:institution waveform 1024
+proposal:user7:instId stringout
+proposal:user7:lastName stringout
+proposal:user7:piFlag bo
+proposal:user7:userId stringout
+proposal:user8:badgeNumber stringout
+proposal:user8:email stringout
+proposal:user8:firstName stringout
+proposal:user8:institution waveform 1024
+proposal:user8:instId stringout
+proposal:user8:lastName stringout
+proposal:user8:piFlag bo
+proposal:user8:userId stringout
+proposal:user9:badgeNumber stringout
+proposal:user9:email stringout
+proposal:user9:firstName stringout
+proposal:user9:institution waveform 1024
+proposal:user9:instId stringout
+proposal:user9:lastName stringout
+proposal:user9:piFlag bo
+proposal:user9:userId stringout
+""".strip().splitlines()
+
+
+def main():
+ """Make an EPICS ``.db`` file from the PVs listed above."""
+ header = """\
+ #
+ # file: apsbss.db
+ # EPICS database for information from APS ESAF & Proposal databases
+ #
+ # BSS: Beamtime Scheduling System
+ #
+ # note: this file autogenerated by make_database.py
+ # ./apsbss_makedb.py | tee apsbss.db
+ #
+ # Start the EPICS IOC with a command (from EPICS base):
+ # softIoc -m P=ioc:bss: -d apsbss.db
+ """
+ for line in header.splitlines():
+ print(line.strip())
+
+ for row in raw_data:
+ parts = row.split()
+ pvname, rtyp = parts[:2]
+ head = f'record({rtyp}, "$(P){pvname}")'
+ fields = []
+ if rtyp == "waveform" and len(parts) == 3:
+ length = parts[-1]
+ fields.append(f'field(FTVL, "CHAR")')
+ fields.append(f'field(NELM, {length})')
+ elif rtyp == "bo":
+ fields.append(f'field(ZNAM, "OFF")')
+ fields.append(f'field(ONAM, "ON")')
+ record = head
+ if len(fields) > 0:
+ field_specs = "\n".join([f" {field}" for field in fields])
+ record += " {\n" + f"{field_specs}" + "\n}"
+ print("\n" + record)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/apstools/beamtime/apsbss_ophyd.py b/apstools/beamtime/apsbss_ophyd.py
new file mode 100644
index 000000000..60272113f
--- /dev/null
+++ b/apstools/beamtime/apsbss_ophyd.py
@@ -0,0 +1,222 @@
+#!/usr/bin/env python
+
+"""
+ophyd support for apsbss
+
+EXAMPLE::
+
+ apsbss = EpicsBssDevice("ioc:bss:", name="apsbss")
+
+.. autosummary::
+
+ ~EpicsBssDevice
+ ~EpicsEsafDevice
+ ~EpicsEsafExperimenterDevice
+ ~EpicsProposalDevice
+ ~EpicsProposalExperimenterDevice
+
+"""
+
+__all__ = ["EpicsBssDevice",]
+
+from ophyd import Component, Device, EpicsSignal
+
+
+class EpicsEsafExperimenterDevice(Device):
+ """
+ Ophyd device for experimenter info from APS ESAF.
+
+ .. autosummary::
+
+ ~clear
+ """
+ badge_number = Component(EpicsSignal, "badgeNumber", string=True)
+ email = Component(EpicsSignal, "email", string=True)
+ first_name = Component(EpicsSignal, "firstName", string=True)
+ last_name = Component(EpicsSignal, "lastName", string=True)
+
+ def clear(self):
+ """Clear the fields for this user."""
+ self.badge_number.put("")
+ self.email.put("")
+ self.first_name.put("")
+ self.last_name.put("")
+
+
+class EpicsEsafDevice(Device):
+ """
+ Ophyd device for info from APS ESAF.
+
+ .. autosummary::
+
+ ~clear
+ ~clear_users
+ """
+
+ aps_cycle = Component(EpicsSignal, "cycle", string=True)
+ description = Component(EpicsSignal, "description", string=True)
+ end_date = Component(EpicsSignal, "endDate", string=True)
+ esaf_id = Component(EpicsSignal, "id", string=True)
+ esaf_status = Component(EpicsSignal, "status", string=True)
+ sector = Component(EpicsSignal, "sector", string=True)
+ start_date = Component(EpicsSignal, "startDate", string=True)
+ title = Component(EpicsSignal, "title", string=True)
+ user_last_names = Component(EpicsSignal, "users", string=True)
+ user_badges = Component(EpicsSignal, "userBadges", string=True)
+
+ _max_users = 9 # 9 users at most?
+ user1 = Component(EpicsEsafExperimenterDevice, "user1:")
+ user2 = Component(EpicsEsafExperimenterDevice, "user2:")
+ user3 = Component(EpicsEsafExperimenterDevice, "user3:")
+ user4 = Component(EpicsEsafExperimenterDevice, "user4:")
+ user5 = Component(EpicsEsafExperimenterDevice, "user5:")
+ user6 = Component(EpicsEsafExperimenterDevice, "user6:")
+ user7 = Component(EpicsEsafExperimenterDevice, "user7:")
+ user8 = Component(EpicsEsafExperimenterDevice, "user8:")
+ user9 = Component(EpicsEsafExperimenterDevice, "user9:")
+
+ def clear(self):
+ """
+ Clear the most of the ESAF info.
+
+ Do not clear these items:
+
+ * ``aps_cycle``
+ * ``esaf_id``
+ * ``sector``
+ """
+ # self.aps_cycle.put("") # user controls this
+ self.description.put("")
+ self.end_date.put("")
+ # self.esaf_id.put("") # user controls this
+ self.esaf_status.put("")
+ # self.sector.put("")
+ self.start_date.put("")
+ self.title.put("")
+ self.user_last_names.put("")
+ self.user_badges.put("")
+
+ self.clear_users()
+
+ def clear_users(self):
+ """Clear the info for all users."""
+ self.user1.clear()
+ self.user2.clear()
+ self.user3.clear()
+ self.user4.clear()
+ self.user5.clear()
+ self.user6.clear()
+ self.user7.clear()
+ self.user8.clear()
+ self.user9.clear()
+
+
+class EpicsProposalExperimenterDevice(Device):
+ """
+ Ophyd device for experimenter info from APS Proposal.
+
+ .. autosummary::
+
+ ~clear
+ """
+ badge_number = Component(EpicsSignal, "badgeNumber", string=True)
+ email = Component(EpicsSignal, "email", string=True)
+ first_name = Component(EpicsSignal, "firstName", string=True)
+ institution = Component(EpicsSignal, "institution", string=True)
+ institution_id = Component(EpicsSignal, "instId", string=True)
+ last_name = Component(EpicsSignal, "lastName", string=True)
+ pi_flag = Component(EpicsSignal, "piFlag", string=True)
+ user_id = Component(EpicsSignal, "userId", string=True)
+
+ def clear(self):
+ """Clear the info for this user."""
+ self.badge_number.put("")
+ self.email.put("")
+ self.first_name.put("")
+ self.last_name.put("")
+ self.user_id.put("")
+ self.institution_id.put("")
+ self.institution.put("")
+ self.pi_flag.put(0)
+
+
+class EpicsProposalDevice(Device):
+ """
+ Ophyd device for info from APS Proposal.
+
+ .. autosummary::
+
+ ~clear
+ ~clear_users
+ """
+
+ beamline_name = Component(EpicsSignal, "beamline", string=True)
+
+ mail_in_flag = Component(EpicsSignal, "mailInFlag", string=True)
+ proposal_id = Component(EpicsSignal, "id", string=True)
+ proprietary_flag = Component(EpicsSignal, "proprietaryFlag", string=True)
+ submitted_date = Component(EpicsSignal, "submittedDate", string=True)
+ title = Component(EpicsSignal, "title", string=True)
+ user_last_names = Component(EpicsSignal, "users", string=True)
+ user_badges = Component(EpicsSignal, "userBadges", string=True)
+
+ _max_users = 9 # 9 users at most?
+ user1 = Component(EpicsProposalExperimenterDevice, "user1:")
+ user2 = Component(EpicsProposalExperimenterDevice, "user2:")
+ user3 = Component(EpicsProposalExperimenterDevice, "user3:")
+ user4 = Component(EpicsProposalExperimenterDevice, "user4:")
+ user5 = Component(EpicsProposalExperimenterDevice, "user5:")
+ user6 = Component(EpicsProposalExperimenterDevice, "user6:")
+ user7 = Component(EpicsProposalExperimenterDevice, "user7:")
+ user8 = Component(EpicsProposalExperimenterDevice, "user8:")
+ user9 = Component(EpicsProposalExperimenterDevice, "user9:")
+
+ def clear(self):
+ """
+ Clear the most of the proposal info.
+
+ Do not clear these items:
+
+ * ``beamline_name``
+ * ``proposal_id``
+ """
+ # self.beamline_name.put("") # user controls this
+ self.mail_in_flag.put(0)
+ # self.proposal_id.put(-1) # user controls this
+ self.proprietary_flag.put(0)
+ self.submitted_date.put("")
+ self.title.put("")
+ self.user_last_names.put("")
+ self.user_badges.put("")
+
+ self.clear_users()
+
+ def clear_users(self):
+ """Clear the info for all users."""
+ self.user1.clear()
+ self.user2.clear()
+ self.user3.clear()
+ self.user4.clear()
+ self.user5.clear()
+ self.user6.clear()
+ self.user7.clear()
+ self.user8.clear()
+ self.user9.clear()
+
+
+class EpicsBssDevice(Device):
+ """
+ Ophyd device for info from APS Proposal and ESAF databases.
+
+ .. autosummary::
+
+ ~clear
+ """
+
+ esaf = Component(EpicsEsafDevice, "esaf:")
+ proposal = Component(EpicsProposalDevice, "proposal:")
+
+ def clear(self):
+ """Clear the proposal and ESAF info."""
+ self.esaf.clear()
+ self.proposal.clear()
diff --git a/apstools/devices.py b/apstools/devices.py
index 71efdfbd5..3f3751265 100644
--- a/apstools/devices.py
+++ b/apstools/devices.py
@@ -1856,6 +1856,7 @@ def generate_datum(self, key, timestamp, datum_kwargs):
datum_kwargs["HDF5_file_name"] = hdf5_file_name
logger.debug("make_filename: %s", hdf5_file_name)
+ logger.debug("write_path: %s", write_path)
return super().generate_datum(key, timestamp, datum_kwargs)
def get_frames_per_point(self):
diff --git a/apstools/filewriters.py b/apstools/filewriters.py
index 95eaf8509..33a27bd7c 100644
--- a/apstools/filewriters.py
+++ b/apstools/filewriters.py
@@ -46,6 +46,7 @@
SPEC_TIME_FORMAT = "%a %b %d %H:%M:%S %Y"
SCAN_ID_RESET_VALUE = 0
+
def _rebuild_scan_command(doc):
"""
reconstruct the scan command for SPEC data file #S line
@@ -657,7 +658,7 @@ def myPlan():
#C Mon Jan 28 12:48:14 2019. exit_status = success
"""
- # global specwriter # such as: specwriter = SpecWriterCallback()
+ global specwriter # such as: specwriter = SpecWriterCallback()
writer = writer or specwriter
if doc is None:
if writer.scanning:
diff --git a/apstools/plans.py b/apstools/plans.py
index 96a073c7f..5eedb9045 100644
--- a/apstools/plans.py
+++ b/apstools/plans.py
@@ -87,7 +87,7 @@ def addDeviceDataAsStream(devices, label):
yield from bps.save()
-def execute_command_list(filename, commands, md={}):
+def execute_command_list(filename, commands, md=None):
"""
plan: execute the command list
@@ -206,7 +206,7 @@ def get_command_list(filename):
def lineup(
counter, axis, minus, plus, npts,
time_s=0.1, peak_factor=4, width_factor=0.8,
- _md={}):
+ md=None):
"""
lineup and center a given axis, relative to current position
@@ -312,16 +312,16 @@ def peak_analysis():
bec.peaks.aligned = aligned
bec.peaks.ATTRS = ('com', 'cen', 'max', 'min', 'fwhm')
- md = dict(_md)
- md["purpose"] = "alignment"
- yield from bp.rel_scan([counter], axis, minus, plus, npts, md=md)
+ _md = dict(purpose="alignment")
+ _md.update(md or {})
+ yield from bp.rel_scan([counter], axis, minus, plus, npts, md=_md)
yield from peak_analysis()
if bec.peaks.aligned:
# again, tweak axis to maximize
- md["purpose"] = "alignment - fine"
+ _md["purpose"] = "alignment - fine"
fwhm = bec.peaks["fwhm"][counter.name]
- yield from bp.rel_scan([counter], axis, -fwhm, fwhm, npts, md=md)
+ yield from bp.rel_scan([counter], axis, -fwhm, fwhm, npts, md=_md)
yield from peak_analysis()
if scaler is not None:
@@ -594,7 +594,7 @@ def register_command_handler(handler=None):
_COMMAND_HANDLER_ = handler or execute_command_list
-def run_command_file(filename, md={}):
+def run_command_file(filename, md=None):
"""
plan: execute a list of commands from a text or Excel file
@@ -614,8 +614,10 @@ def run_command_file(filename, md={}):
*new in apstools release 1.1.7*
"""
+ _md = dict(command_file=filename)
+ _md.update(md or {})
commands = get_command_list(filename)
- yield from _COMMAND_HANDLER_(filename, commands)
+ yield from _COMMAND_HANDLER_(filename, commands, md=_md)
def snapshot(obj_list, stream="primary", md=None):
@@ -738,7 +740,7 @@ def sscan_1D(
running_stream="primary",
final_array_stream=None,
device_settings_stream="settings",
- md={}):
+ md=None):
"""
simple 1-D scan using EPICS synApps sscan record
@@ -836,9 +838,10 @@ def phase_cb(value, timestamp, **kwargs):
# watch for new data to be read out
sscan.scan_phase.subscribe(phase_cb)
- md["plan_name"] = "sscan_1D"
+ _md = dict(plan_name="sscan_1D")
+ _md.update(md or {})
- yield from bps.open_run(md) # start data collection
+ yield from bps.open_run(_md) # start data collection
yield from bps.mv(sscan.execute_scan, 1) # start sscan
started = True
diff --git a/apstools/utils.py b/apstools/utils.py
index 3493b7892..305602911 100644
--- a/apstools/utils.py
+++ b/apstools/utils.py
@@ -30,6 +30,7 @@
~safe_ophyd_name
~show_ophyd_symbols
~split_quoted_line
+ ~summarize_runs
~text_encode
~to_unicode_or_bust
~trim_string_for_EPICS
@@ -49,8 +50,9 @@
from bluesky.callbacks.best_effort import BestEffortCallback
from bluesky import plan_stubs as bps
-from collections import OrderedDict
+from collections import defaultdict, OrderedDict
import databroker
+import databroker.queries
import datetime
from email.mime.text import MIMEText
from event_model import NumpyEncoder
@@ -237,7 +239,7 @@ def itemizer(fmt, items):
def listruns(
- num=20, keys=[], printing=True,
+ num=20, keys=None, printing=True,
show_command=True, db=None,
exit_status=None,
**db_search_terms):
@@ -308,6 +310,7 @@ def listruns(
*new in apstools release 1.1.10*
"""
db = db or ipython_shell_namespace()["db"]
+ keys = keys or []
if show_command:
labels = "scan_id command".split() + keys
@@ -726,6 +729,65 @@ def split_quoted_line(line):
return parts
+def summarize_runs(since=None, db=None):
+ """
+ Report bluesky run metrics from the databroker.
+
+ * How many different plans?
+ * How many runs?
+ * How many times each run was used?
+ * How frequently? (TODO:)
+
+ PARAMETERS
+
+ since (str) :
+ Report all runs since this ISO8601 date & time (default: ``1995``)
+ db (object) :
+ Instance of ``databroker.Broker()``
+ (default: ``db`` from the IPython shell)
+ """
+ db = db or ipython_shell_namespace()["db"]
+ since = since or "1995" # no APS X-ray experiment data before 1995!
+ cat = db.v2.search(databroker.queries.TimeRange(since=since))
+ plans = defaultdict(list)
+ t0 = time.time()
+ for n, uid in enumerate(cat):
+ t1 = time.time()
+ run = cat[uid] # this step is very slow (0.01 - 0.5 seconds each!)
+ t2 = time.time()
+ plan_name = run.metadata["start"].get("plan_name", "unknown")
+ dt = datetime.datetime.fromtimestamp(run.metadata["start"]["time"]).isoformat()
+ scan_id = run.metadata["start"].get("scan_id", "unknown")
+ plans[plan_name].append(
+ dict(
+ plan_name=plan_name,
+ dt=dt,
+ time_start=dt,
+ uid=uid,
+ scan_id=scan_id,
+ )
+ )
+ logger.debug(
+ "%s %s dt1=%4.01fus dt2=%5.01fms %s",
+ scan_id,
+ dt,
+ (t1-t0)*1e6,
+ (t2-t1)*1e3,
+ plan_name,
+ )
+ t0 = time.time()
+
+ def sorter(plan_name):
+ return len(plans[plan_name])
+
+ table = pyRestTable.Table()
+ table.labels = "plan quantity".split()
+ for k in sorted(plans.keys(), key=sorter, reverse=True):
+ table.addRow((k, sorter(k)))
+ table.addRow(("TOTAL", n+1))
+ print(table)
+
+
def text_encode(source):
"""Encode ``source`` using the default codepoint."""
return source.encode(errors='ignore')
diff --git a/conda-recipe/meta.yaml b/conda-recipe/meta.yaml
index 7a0368df9..d4e579c52 100644
--- a/conda-recipe/meta.yaml
+++ b/conda-recipe/meta.yaml
@@ -26,6 +26,7 @@ build:
script: python -m pip install --no-deps --ignore-installed .
noarch: python
entry_points:
+ - apsbss = apstools.beamtime.apsbss:main
- apstools_plan_catalog = apstools.examples:main
- bluesky_snapshot = apstools.snapshot:snapshot_cli
- bluesky_snapshot_viewer = apstools.snapshot:snapshot_gui
@@ -35,9 +36,11 @@ requirements:
- python
- pip
run:
- - python
+ - python>=3.6.0
+ - aps-dm-api
- bluesky>=1.6.2
- databroker>=1.0.6
+ - h5py
- ophyd>=1.5.1
- pandas
- pyEpics>=3.4.2
diff --git a/create_release_notes.py b/create_release_notes.py
index f4dda3445..b368171fa 100755
--- a/create_release_notes.py
+++ b/create_release_notes.py
@@ -37,13 +37,16 @@ def findGitConfigFile():
Needs testing for when things are wrong.
"""
path = os.getcwd()
- for i in range(99):
+ while len(path.split(os.path.sep)):
config_file = os.path.join(path, ".git", "config")
if os.path.exists(config_file):
return config_file # found it!
# next, look in the parent directory
- path = os.path.abspath(os.path.join(path, ".."))
+ parent = os.path.dirname(path)
+ if parent == path:
+ break
+ path = parent
msg = "Could not find .git/config file in any parent directory."
logger.error(msg)
diff --git a/docs/source/applications/apsbss.rst b/docs/source/applications/apsbss.rst
new file mode 100644
index 000000000..a1e55ed13
--- /dev/null
+++ b/docs/source/applications/apsbss.rst
@@ -0,0 +1,450 @@
+
+
+.. index:: apsbss
+
+.. _apsbss_application:
+
+apsbss
+------
+
+Provide information from APS Proposal and ESAF (experiment safety approval
+form) databases as PVs at each beam line so that this information
+(metadata) may be added to new data files. The ``aps-dm-api``
+(``dm`` for short) package [#]_
+is used to access the APS databases as read-only.
+
+*No information is written back to the APS
+databases from this software.*
+
+.. [#] ``dm``: https://anaconda.org/search?q=aps-dm-api
+
+.. sidebar:: PVs for experiment metadata
+
+ This information retreived from the APS databases is stored in PVs
+ on the beam line subnet. These PVs are available to *any* EPICS
+ client as metadata (SPEC, area detector, Bluesky, GUI screens, logging, other).
+ This design allows the local instrument team to override
+ any values read from the APS databases, if that is needed.
+
+Given:
+
+* a beam line name (such as ``9-ID-B,C``)
+* APS run cycle name (such as ``2019-2``) to locate a specific proposal ID
+* proposal ID number (such as ``66083``)
+* ESAF ID number (such as ``226319``)
+
+The typical information obtained includes:
+
+* ESAF & proposal titles
+* user names
+* user institutional affiliations
+* user emails
+* applicable dates, reported in ISO8601 time format
+* is proposal propietary?
+* is experiment mail-in?
+
+These PVs are loaded on demand by the local instrument team at the beam line.
+See the :ref:`apsbss_startup` section for details about
+managing the EPICS PVs.
+
+
+Overview
+++++++++
+
+We'll demonstrate ``apsbss`` with information for APS beam
+line 9-ID, using PV prefix ``9id:bss:``.
+
+#. Create the PVs in an EPICS IOC
+#. Initialize PVs with beam line name and APS run cycle number
+#. Set PVs with the Proposal and ESAF ID numbers
+#. Retrieve (& update PVs) information from APS databases
+
+**Enter beam line and APS run (cycle) info**
+
+.. figure:: ../resources/ui_initialized.png
+ :width: 95%
+
+ Image of ``apsbss.ui`` screen GUI in caQtDM showing PV prefix
+ (``9id:bss:``), APS run cycle ``2019-2`` and beam line ``9-ID-B,C``.
+
+ * beam line name PV: ``9id:bss:proposal:beamline``
+ * APS run cycle PV: ``9id:bss:esaf:cycle``
+
+
+**Enter Proposal and ESAF ID numbers**
+
+Note we had to use the APS run cycle of `2019-2`
+to match what is in the proposal's information.
+
+.. figure:: ../resources/ui_id_entered.png
+ :width: 95%
+
+ Image of ``apsbss.ui`` screen GUI in caQtDM with Proposal
+ and ESAF ID numbers added.
+
+ * proposal ID number PV: ``9id:bss:proposal:id``
+ * ESAF ID number PV: ``9id:bss:esaf:id``
+
+**Update PVs from APS databases**
+
+In the GUI, press the button labeled ``get Proposal and ESAF info``.
+This button executes the command line: ``apsbss update 9id:bss:``
+
+Here's a view of the GUI after running the update. The
+information shown in the GUI is only part of the PVs,
+presented in a compact format. A full report of the
+information received, including PV names, is available for
+:download:`download <../resources/apsbss_report.txt>`.
+
+.. figure:: ../resources/ui_updated.png
+ :width: 95%
+
+ Image of ``apsbss.ui`` screen GUI in caQtDM showing Proposal
+ and ESAF information.
+
+To clear the PVs, in the GUI, press the button labeled ``clear PVs``.
+This button executes the command line: ``apsbss clear 9id:bss:``
+
+
+Initialize PVs for beam line and APC run cycle
+++++++++++++++++++++++++++++++++++++++++++++++
+
+After creating the PVs in an IOC, the next step is to
+initialize them with the beam line name and the APS
+run cycle name. Both of these must match exactly
+with values known in the data management (``dm``) system.
+
+For any of these commands, you must know the EPICS
+PV prefix to be used. The examples above are for
+beam line 9-ID. The PV prefix in these examples
+is ``9id:bss:``.
+
+
+What beam line name to use?
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+To learn the beam line names accepted by the system, use this command
+(showing names defined on 2020-07-10)::
+
+ $ apsbss beamlines
+ 1-BM-B,C 8-ID-I 15-ID-B,C,D 23-BM-B
+ 1-ID-B,C,E 9-BM-B,C 16-BM-B 23-ID-B
+ 2-BM-A,B 9-ID-B,C 16-BM-D 23-ID-D
+ 2-ID-D 10-BM-A,B 16-ID-B 24-ID-C
+ 2-ID-E 10-ID-B 16-ID-D 24-ID-E
+ 3-ID-B,C,D 11-BM-B 17-BM-B 26-ID-C
+ 4-ID-C 11-ID-B 17-ID-B 27-ID-B
+ 4-ID-D 11-ID-C 18-ID-D 29-ID-C,D
+ 5-BM-C 11-ID-D 19-BM-D 30-ID-B,C
+ 5-BM-D 12-BM-B 19-ID-D 31-ID-D
+ 5-ID-B,C,D 12-ID-B 20-BM-B 32-ID-B,C
+ 6-BM-A,B 12-ID-C,D 20-ID-B,C 33-BM-C
+ 6-ID-B,C 13-BM-C 21-ID-D 33-ID-D,E
+ 6-ID-D 13-BM-D 21-ID-E 34-ID-C
+ 7-BM-B 13-ID-C,D 21-ID-F 34-ID-E
+ 7-ID-B,C,D 13-ID-E 21-ID-G 35-ID-B,C,D,E
+ 8-BM-B 14-BM-C 22-BM-D
+ 8-ID-E 14-ID-B 22-ID-D
+
+For either station at 9-ID, use ``9-ID-B,C``.
+
+
+What APS run cycle to use?
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+To learn the APS run cycle names accepted by the system, use this command
+(showing APS run cycle names defined on 2020-07-10)::
+
+ $ apsbss cycles
+ 2008-3 2011-2 2014-1 2016-3 2019-2
+ 2009-1 2011-3 2014-2 2017-1 2019-3
+ 2009-2 2012-1 2014-3 2017-2 2020-1
+ 2009-3 2012-2 2015-1 2017-3 2020-2
+ 2010-1 2012-3 2015-2 2018-1
+ 2010-2 2013-1 2015-3 2018-2
+ 2010-3 2013-2 2016-1 2018-3
+ 2011-1 2013-3 2016-2 2019-1
+
+Pick the cycle of interest. Here, we pick ``2020-2``.
+
+To print the full report (including start and end of each cycle)::
+
+ $ apsbss cycles --full
+ ====== =================== ===================
+ cycle start end
+ ====== =================== ===================
+ 2020-2 2020-06-09 07:00:00 2020-10-01 07:00:00
+ 2020-1 2020-01-28 08:00:00 2020-06-09 07:00:00
+ 2019-3 2019-09-24 07:00:00 2020-01-28 08:00:00
+ 2019-2 2019-05-21 07:00:00 2019-09-24 07:00:00
+ 2019-1 2019-01-22 08:00:00 2019-05-21 07:00:00
+ 2018-3 2018-09-25 07:00:00 2019-01-22 08:00:00
+ 2018-2 2018-05-22 07:00:00 2018-09-25 07:00:00
+ 2018-1 2018-01-23 08:00:00 2018-05-22 07:00:00
+ 2017-3 2017-09-26 07:00:00 2018-01-23 08:00:00
+ 2017-2 2017-05-23 07:00:00 2017-09-26 07:00:00
+ 2017-1 2017-01-24 08:00:00 2017-05-23 07:00:00
+ 2016-3 2016-09-27 07:00:00 2017-01-24 08:00:00
+ 2016-2 2016-05-24 07:00:00 2016-09-27 07:00:00
+ 2016-1 2016-01-26 08:00:00 2016-05-24 07:00:00
+ 2015-3 2015-09-29 07:00:00 2016-01-26 08:00:00
+ 2015-2 2015-05-26 07:00:00 2015-09-29 07:00:00
+ 2015-1 2015-01-27 08:00:00 2015-05-26 07:00:00
+ 2014-3 2014-09-25 07:00:00 2015-01-27 08:00:00
+ 2014-2 2014-05-20 07:00:00 2014-09-25 07:00:00
+ 2014-1 2014-01-21 08:00:00 2014-05-20 07:00:00
+ 2013-3 2013-09-24 07:00:00 2014-01-21 08:00:00
+ 2013-2 2013-05-22 07:00:00 2013-09-24 07:00:00
+ 2013-1 2013-01-22 08:00:00 2013-05-22 07:00:00
+ 2012-3 2012-09-25 07:00:00 2013-01-22 08:00:00
+ 2012-2 2012-05-23 07:00:00 2012-09-25 07:00:00
+ 2012-1 2012-01-24 08:00:00 2012-05-23 07:00:00
+ 2011-3 2011-09-27 07:00:00 2012-01-24 08:00:00
+ 2011-2 2011-05-25 07:00:00 2011-09-27 07:00:00
+ 2011-1 2011-01-25 08:00:00 2011-05-25 07:00:00
+ 2010-3 2010-09-27 23:00:00 2011-01-25 08:00:00
+ 2010-2 2010-05-26 07:00:00 2010-09-28 07:00:00
+ 2010-1 2010-01-26 08:00:00 2010-05-26 07:00:00
+ 2009-3 2009-09-29 07:00:00 2010-01-26 08:00:00
+ 2009-2 2009-05-20 07:00:00 2009-09-29 07:00:00
+ 2009-1 2009-01-21 08:00:00 2009-05-20 07:00:00
+ 2008-3 2008-09-24 07:00:00 2009-01-21 08:00:00
+ ====== =================== ===================
+
+
+Write the beam line name and cycle to the PVs
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+To configure ``9id:bss:`` PVs for beam line
+``9-ID-B,C`` and cycle ``2020-2``,
+use this command::
+
+ $ apsbss setup 9id:bss: 9-ID-B,C 2020-2
+ connected in 0.143s
+ setup EPICS 9id:bss: 9-ID-B,C cycle=2020-2 sector=9
+
+Or you could enter them into the appropriate boxes on the GUI.
+
+
+What Proposal and ESAF ID numbers to use?
++++++++++++++++++++++++++++++++++++++++++
+
+Proposals are usually valid for two years. To learn what
+proposals are valid for your beam line, use this command
+with your own beam line's name. The report will provide
+two tables, one for ESAFs for the current cycle and the
+other for proposals
+within the last two years (6 APS cycles)::
+
+ $ apsbss current 9id:bss: 9-ID-B,C
+ Current Proposal(s) on 9-ID-B,C
+
+ ===== ====== =================== ==================== ========================================
+ id cycle date user(s) title
+ ===== ====== =================== ==================== ========================================
+ 57504 2019-3 2017-10-27 15:31:46 Zhang,Levine,Long... Towards USAXS/SAXS/WAXS Characterizat...
+ 55236 2019-2 2017-07-07 12:32:39 Du,Vacek,Syed,Hon... Developing 3D cryo ptychography at th...
+ 64629 2019-2 2019-03-01 18:35:02 Ilavsky,Okasinski 2019 National School on Neutron & X-r...
+ 62490 2019-1 2018-10-25 11:10:49 Ilavsky,Frith,Sun Dissolution of nano-precipitates in m...
+ ===== ====== =================== ==================== ========================================
+
+ Current ESAF(s) on sector 9
+
+ ====== ======== ========== ========== ==================== ========================================
+ id status start end user(s) title
+ ====== ======== ========== ========== ==================== ========================================
+ 221805 Approved 2020-02-18 2020-12-25 Chen,Deng,Yao,Jia... Bionanoprobe commissioning
+ 226319 Approved 2020-05-26 2020-09-28 Ilavsky,Maxey,Kuz... Commission 9ID and USAXS
+ 226572 Approved 2020-06-10 2020-09-28 Sterbinsky,Heald,... 9BM Beamline Commissioning 2020-2
+ 226612 Approved 2020-06-10 2020-09-28 Chen,Deng,Yao,Jia... Bionanoprobe commissioning
+ ====== ======== ========== ========== ==================== ========================================
+
+Note that some of the information in the tables above has been removed for brevity.
+
+
+View Proposal Information
++++++++++++++++++++++++++
+
+To view information about a specific proposal, you
+must be able to provide the proposal's ID number and
+the APS run cycle name.
+
+::
+
+ $ apsbss proposal 64629 2019-2 9-ID-B,C
+ duration: 36000
+ endTime: '2019-06-25 17:00:00'
+ experimenters:
+ - badge: 'text_number_here'
+ email: uuuuuuuuuu@email.fqdn
+ firstName: Jan
+ id: number_here
+ instId: 3927
+ institution: Argonne National Laboratory
+ lastName: Ilavsky
+ - badge: 'text_number_here'
+ email: uuuuuuuuuu@email.fqdn
+ firstName: John
+ id: number_here
+ instId: 3927
+ institution: Argonne National Laboratory
+ lastName: Okasinski
+ piFlag: Y
+ id: 64629
+ mailInFlag: N
+ proprietaryFlag: N
+ startTime: '2019-06-25 07:00:00'
+ submittedDate: '2019-03-01 18:35:02'
+ title: 2019 National School on Neutron & X-ray Scattering Beamline Practicals - CMS
+ totalShiftsRequested: 12
+
+
+Get ESAF Information
+++++++++++++++++++++
+
+To view information about a specific ESAF, you
+must be able to provide the ESAF ID number.
+
+::
+
+ $ apsbss esaf 226319
+ description: We will commission beamline and USAXS instrument. We will perform experiments
+ with safe beamline standards and test samples (all located at beamline and used
+ for this purpose routinely) to evaluate performance of beamline and instrument.
+ We will perform hardware and software development as needed.
+ esafId: 226319
+ esafStatus: Approved
+ esafTitle: Commission 9ID and USAXS
+ experimentEndDate: '2020-09-28 08:00:00'
+ experimentStartDate: '2020-05-26 08:00:00'
+ experimentUsers:
+ - badge: 'text_number_here'
+ badgeNumber: 'text_number_here'
+ email: uuuuuuuuuu@email.fqdn
+ firstName: Jan
+ lastName: Ilavsky
+ - badge: 'text_number_here'
+ badgeNumber: 'text_number_here'
+ email: uuuuuuuuuu@email.fqdn
+ firstName: Evan
+ lastName: Maxey
+ - badge: 'text_number_here'
+ badgeNumber: 'text_number_here'
+ email: uuuuuuuuuu@email.fqdn
+ firstName: Ivan
+ lastName: Kuzmenko
+ sector: 09
+
+
+Update EPICS PVs with Proposal and ESAF
++++++++++++++++++++++++++++++++++++++++
+
+To update the PVs with Proposal and Information from the APS
+database, first enter the proposal and ESAF ID numbers into
+the GUI (or set the ``9id:bss:proposal:id``, and respectively).
+Note that for this ESAF ID, we had to change the cycle to `2019-2`.
+
+Then, use this command to retrieve the information and update
+the PVs::
+
+ $ apsbss update 9id:bss:
+ update EPICS 9id:bss:
+ connected in 0.105s
+
+
+Clear the EPICS PVs
++++++++++++++++++++
+
+To clear the information from the PVs, use this command::
+
+ $ apsbss clear 9id:bss:
+ clear EPICS 9id:bss:
+ connected in 0.104s
+ cleared in 0.011s
+
+
+Report information in the EPICS PVs
++++++++++++++++++++++++++++++++++++
+
+To view all the information in the PVs, use this command::
+
+ $ apsbss report 9id:bss:
+ clear EPICS 9id:bss:
+
+Since this content is rather large, it is available
+for download: :download:`apsbss report <../resources/apsbss_report.txt>`
+
+
+Example - ``apsbss`` command line
++++++++++++++++++++++++++++++++++
+
+Before using the command-line interface, find out what
+the *apsbss* application expects::
+
+ $ apsbss -h
+ usage: apsbss [-h] [-v]
+ {beamlines,current,cycles,esaf,proposal,clear,setup,update,report}
+ ...
+
+ Retrieve specific records from the APS Proposal and ESAF databases.
+
+ optional arguments:
+ -h, --help show this help message and exit
+ -v, --version print version number and exit
+
+ subcommand:
+ {beamlines,current,cycles,esaf,proposal,clear,setup,update,report}
+ beamlines print list of beamlines
+ current print current ESAF(s) and proposal(s)
+ cycles print APS run cycle names
+ esaf print specific ESAF
+ proposal print specific proposal
+ clear EPICS PVs: clear
+ setup EPICS PVs: setup
+ update EPICS PVs: update from BSS
+ report EPICS PVs: report what is in the PVs
+
+See :ref:`beamtime_source_docs` for the source code documentation
+of each of these subcommands.
+
+.. _apsbss_epics_gui_screens:
+
+Displays for MEDM & caQtDM
+++++++++++++++++++++++++++
+
+Display screen files are provided for viewing some of the EPICS PVs
+using either MEDM (``apsbss.adl``) or caQtDM (``apsbss.ui``).
+
+* caQtDM screen: :download:`apsbss.ui <../../../apstools/beamtime/apsbss.ui>`
+* MEDM screen: :download:`apsbss.adl <../../../apstools/beamtime/apsbss.adl>`
+
+Start caQtDM with this command: ``caQtDM -macro "P=9id:bss:" apsbss.ui &``
+
+Start MEDM with this command: ``medm -x -macro "P=9id:bss:" apsbss.ui &``
+
+IOC Management
+++++++++++++++
+
+The EPICS PVs are provided by running an instance of ``apsbss.db``
+either in an existing EPICS IOC or using the ``softIoc`` application
+from EPICS base. A shell script (``apsbss_ioc.sh``) is included
+for loading Proposal and ESAF information from the
+APS databases into the IOC.
+
+* :download:`apsbss.db <../../../apstools/beamtime/apsbss.db>`
+
+See the section titled ":ref:`apsbss_startup`"
+for the management of the EPICS IOC.
+
+Downloads
++++++++++
+
+* EPICS database: :download:`apsbss.db <../../../apstools/beamtime/apsbss.db>`
+* EPICS IOC shell script :download:`apsbss_ioc.sh <../../../apstools/beamtime/apsbss_ioc.sh>`
+* MEDM screen: :download:`apsbss.adl <../../../apstools/beamtime/apsbss.adl>`
+* caQtDM screen: :download:`apsbss.ui <../../../apstools/beamtime/apsbss.ui>`
+
+Source code documentation
++++++++++++++++++++++++++
+
+See :ref:`beamtime_source_docs` for the source code documentation.
diff --git a/docs/source/applications/apsbss_ioc.rst b/docs/source/applications/apsbss_ioc.rst
new file mode 100644
index 000000000..a5efe41b6
--- /dev/null
+++ b/docs/source/applications/apsbss_ioc.rst
@@ -0,0 +1,165 @@
+.. _apsbss_startup:
+
+apsbss: IOC Startup and Management
+==================================
+
+The :ref:`apsbss_application` software
+provides information from the APS Proposal
+and ESAF (experiment safety approval
+form) databases as PVs to the local controls network.
+The ``aps-dm-api`` (``dm`` for short) package [#]_
+is used to access the APS databases as read-only.
+The information in the PVs can be used as metadata
+for inclusion in data files produced
+
+*No information is written back to the APS
+databases from this software.*
+
+.. [#] ``dm``: https://anaconda.org/search?q=aps-dm-api
+
+
+Overview
+--------
+
+#. Create the PVs in an EPICS IOC
+#. Initialize PVs with beam line name and APS run cycle number
+#. Set PV with the Proposal ID number
+#. Set PV with the ESAF ID number
+#. Retrieve (& update PVs) information from APS databases
+
+
+.. _apsbss_ioc_management:
+
+Start EPICS IOC
+---------------
+
+The EPICS Process Variables (PVs) that support this software
+are provided by an EPICS PV server (IOC). The PVs are defined
+by including the ``apsbss.db`` database file in the startup
+of the IOC. The database can be add to an existing IOC
+or run as a stand-alone IOC using the ``softIoc`` application
+from EPICS base.
+
+To ensure that we create PVs with unique names, decide what
+prefix to use with the EPICS database. Such as, for APS beam
+line 9-ID, you might pick: ``9id:bss:`` (making sure to end
+the prefix with the customary ``:``).
+
+Add EPICS database to existing IOC
+++++++++++++++++++++++++++++++++++
+
+To include ``apsbss.db`` in an existing IOC, copy the file
+(see Downloads below) into the IOC's startup directory
+(typically the directory with ``st.cmd``). Edit the ``st.cmd``
+file and a line like this just before the call to ``iocInit``::
+
+ dbLoadRecords("apsbss.db", "P=9id:bss:")
+
+Once the IOC is started, these PVs should be available to any
+EPICS client on the local network.
+
+Run EPICS database in softIoc from EPICS Base
++++++++++++++++++++++++++++++++++++++++++++++
+
+For this example, we pick a unique name for this process,
+(``ioc9idbss``) based on the PV prefix (``9id:bss:``).
+
+.. sidebar:: TIP
+
+ If you customize your copy of ``apsbss_ioc.sh``
+ and pre-define the two lines::
+
+ # change the program defaults here
+ DEFAULT_SESSION_NAME=apsbss
+ DEFAULT_IOC_PREFIX=ioc:bss:
+
+ then you do not need to supply these terms as
+ command-line arguments. The usage command
+ will show these the default names you provided.
+
+**Start** the IOC using the ``apsbss_ioc.sh`` tool
+(as described below), with this command::
+
+ $ apsbss_ioc.sh start ioc9idbss 9id:bss:
+ Starting ioc9idbss with IOC prefix 9id:bss:
+
+**Stop** the IOC with this command::
+
+ $ apsbss_ioc.sh stop ioc9idbss 9id:bss:
+ ioc9idbss is not running
+
+The IOC **restart** command, will first stop the IOC (if
+it is running), then start it.
+
+Report whether or not the IOC is running with this command::
+
+ $ apsbss_ioc.sh status ioc9idbss 9id:bss:
+ ioc9idbss is not running
+
+To interact with the **console** of an IOC running in
+a ``screen`` session::
+
+ $ apsbss_ioc.sh console ioc9idbss 9id:bss:
+
+To end this interaction, type ``^A`` then ``D`` which will
+leave the IOC running. Type ``exit`` to stop the IOC from
+the console.
+
+For diagnostic (or other) purposes, you can also run the IOC
+without using a screen session. This is the command::
+
+ $ apsbss_ioc.sh run ioc9idbss 9id:bss:
+ dbLoadDatabase("/home/beams1/JEMIAN/.conda/envs/bluesky_2020_5/epics/bin/linux-x86_64/../../dbd/softIoc.dbd")
+ softIoc_registerRecordDeviceDriver(pdbbase)
+ dbLoadRecords("apsbss.db", "P=9id:bss:")
+ iocInit()
+ Starting iocInit
+ ############################################################################
+ ## EPICS R7.0.4
+ ## Rev. 2020-05-29T13:39+0000
+ ############################################################################
+ cas warning: Configured TCP port was unavailable.
+ cas warning: Using dynamically assigned TCP port 39809,
+ cas warning: but now two or more servers share the same UDP port.
+ cas warning: Depending on your IP kernel this server may not be
+ cas warning: reachable with UDP unicast (a host's IP in EPICS_CA_ADDR_LIST)
+ iocRun: All initialization complete
+ epics>
+
+You should see the IOC shell prompt (``epics> ``). If you type ``exit``
+or otherwise close the session, the IOC will exit.
+
+
+Shell Script to manage softIoc
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+To run a stand-alone IOC using the ``softIoc`` application
+from EPICS base, use the supplied IOC management shell script
+``apsbss_ioc.sh``::
+
+ $ apsbss_ioc.sh
+ Usage: apsbss_ioc.sh {start|stop|restart|status|console|run} [NAME [PREFIX]]
+
+ COMMANDS
+ console attach to IOC console if IOC is running in screen
+ restart restart IOC
+ run run IOC in console (not screen)
+ start start IOC
+ status report if IOC is running
+ stop stop IOC
+
+ OPTIONAL TERMS
+ NAME name of IOC session (default: apsbss)
+ PREFIX IOC prefix (default: ioc:bss:)
+
+* :download:`apsbss.db <../../../apstools/beamtime/apsbss.db>`
+* :download:`apsbss_ioc.sh <../../../apstools/beamtime/apsbss_ioc.sh>`
+
+.. note:: The shell script assumes that a working ``softIoc`` application
+ (from EPICS base) is in your executable ``$PATH``. You should confirm
+ this first before trying to start the IOC.
+
+.. note:: The ``softIoc`` application is run within a ``screen``
+ session so that it remains running even if you close the
+ console session. Confirm that you have the ``screen`` application
+ first before trying to start the IOC.
\ No newline at end of file
diff --git a/docs/source/applications/index.rst b/docs/source/applications/index.rst
index 64f2d05aa..affabe88f 100644
--- a/docs/source/applications/index.rst
+++ b/docs/source/applications/index.rst
@@ -8,14 +8,17 @@ Applications
:maxdepth: 2
:glob:
+ apsbss
+ apsbss_ioc
snapshot
spec2ophyd
-There are two command-line applications provided by apstools:
+There are the applications provided by apstools:
===================================================== =================================
application purpose
===================================================== =================================
+:ref:`apsbss_application` Information from the APS Proposal and ESAF databases.
:ref:`apstools_plan_catalog ` summary list of all scans in the databroker
:ref:`bluesky_snapshot` Take a snapshot of a list of EPICS PVs and record it in the databroker.
:ref:`spec2ophyd` read SPEC config file and convert to ophyd setup commands
diff --git a/docs/source/resources/apsbss_report.txt b/docs/source/resources/apsbss_report.txt
new file mode 100644
index 000000000..a39782612
--- /dev/null
+++ b/docs/source/resources/apsbss_report.txt
@@ -0,0 +1,130 @@
+============================= ================================== ==============================
+name PV reference value
+============================= ================================== ==============================
+esaf.aps_cycle 9id:bss:esaf:cycle 2019-2
+esaf.description 9id:bss:esaf:description We will commission beamline an
+esaf.end_date 9id:bss:esaf:endDate 2020-09-28 08:00:00
+esaf.esaf_id 9id:bss:esaf:id 226319
+esaf.esaf_status 9id:bss:esaf:status Approved
+esaf.sector 9id:bss:esaf:sector 9
+esaf.start_date 9id:bss:esaf:startDate 2020-05-26 08:00:00
+esaf.title 9id:bss:esaf:title Commission 9ID and USAXS
+esaf.user1.badge_number 9id:bss:esaf:user1:badgeNumber 86312
+esaf.user1.email 9id:bss:esaf:user1:email ilavsky@aps.anl.gov
+esaf.user1.first_name 9id:bss:esaf:user1:firstName Jan
+esaf.user1.last_name 9id:bss:esaf:user1:lastName Ilavsky
+esaf.user2.badge_number 9id:bss:esaf:user2:badgeNumber 53748
+esaf.user2.email 9id:bss:esaf:user2:email emaxey@aps.anl.gov
+esaf.user2.first_name 9id:bss:esaf:user2:firstName Evan
+esaf.user2.last_name 9id:bss:esaf:user2:lastName Maxey
+esaf.user3.badge_number 9id:bss:esaf:user3:badgeNumber 64065
+esaf.user3.email 9id:bss:esaf:user3:email kuzmenko@aps.anl.gov
+esaf.user3.first_name 9id:bss:esaf:user3:firstName Ivan
+esaf.user3.last_name 9id:bss:esaf:user3:lastName Kuzmenko
+esaf.user4.badge_number 9id:bss:esaf:user4:badgeNumber
+esaf.user4.email 9id:bss:esaf:user4:email
+esaf.user4.first_name 9id:bss:esaf:user4:firstName
+esaf.user4.last_name 9id:bss:esaf:user4:lastName
+esaf.user5.badge_number 9id:bss:esaf:user5:badgeNumber
+esaf.user5.email 9id:bss:esaf:user5:email
+esaf.user5.first_name 9id:bss:esaf:user5:firstName
+esaf.user5.last_name 9id:bss:esaf:user5:lastName
+esaf.user6.badge_number 9id:bss:esaf:user6:badgeNumber
+esaf.user6.email 9id:bss:esaf:user6:email
+esaf.user6.first_name 9id:bss:esaf:user6:firstName
+esaf.user6.last_name 9id:bss:esaf:user6:lastName
+esaf.user7.badge_number 9id:bss:esaf:user7:badgeNumber
+esaf.user7.email 9id:bss:esaf:user7:email
+esaf.user7.first_name 9id:bss:esaf:user7:firstName
+esaf.user7.last_name 9id:bss:esaf:user7:lastName
+esaf.user8.badge_number 9id:bss:esaf:user8:badgeNumber
+esaf.user8.email 9id:bss:esaf:user8:email
+esaf.user8.first_name 9id:bss:esaf:user8:firstName
+esaf.user8.last_name 9id:bss:esaf:user8:lastName
+esaf.user9.badge_number 9id:bss:esaf:user9:badgeNumber
+esaf.user9.email 9id:bss:esaf:user9:email
+esaf.user9.first_name 9id:bss:esaf:user9:firstName
+esaf.user9.last_name 9id:bss:esaf:user9:lastName
+esaf.user_badges 9id:bss:esaf:userBadges 86312,53748,64065
+esaf.user_last_names 9id:bss:esaf:users Ilavsky,Maxey,Kuzmenko
+proposal.beamline_name 9id:bss:proposal:beamline 9-ID-B,C
+proposal.mail_in_flag 9id:bss:proposal:mailInFlag OFF
+proposal.proposal_id 9id:bss:proposal:id 64629
+proposal.proprietary_flag 9id:bss:proposal:proprietaryFlag OFF
+proposal.submitted_date 9id:bss:proposal:submittedDate 2019-03-01 18:35:02
+proposal.title 9id:bss:proposal:title 2019 National School on Neutro
+proposal.user1.badge_number 9id:bss:proposal:user1:badgeNumber 86312
+proposal.user1.email 9id:bss:proposal:user1:email ilavsky@aps.anl.gov
+proposal.user1.first_name 9id:bss:proposal:user1:firstName Jan
+proposal.user1.institution 9id:bss:proposal:user1:institution Argonne National Laboratory
+proposal.user1.institution_id 9id:bss:proposal:user1:instId 3927
+proposal.user1.last_name 9id:bss:proposal:user1:lastName Ilavsky
+proposal.user1.pi_flag 9id:bss:proposal:user1:piFlag OFF
+proposal.user1.user_id 9id:bss:proposal:user1:userId 424292
+proposal.user2.badge_number 9id:bss:proposal:user2:badgeNumber 85283
+proposal.user2.email 9id:bss:proposal:user2:email okasinski@aps.anl.gov
+proposal.user2.first_name 9id:bss:proposal:user2:firstName John
+proposal.user2.institution 9id:bss:proposal:user2:institution Argonne National Laboratory
+proposal.user2.institution_id 9id:bss:proposal:user2:instId 3927
+proposal.user2.last_name 9id:bss:proposal:user2:lastName Okasinski
+proposal.user2.pi_flag 9id:bss:proposal:user2:piFlag ON
+proposal.user2.user_id 9id:bss:proposal:user2:userId 424308
+proposal.user3.badge_number 9id:bss:proposal:user3:badgeNumber
+proposal.user3.email 9id:bss:proposal:user3:email
+proposal.user3.first_name 9id:bss:proposal:user3:firstName
+proposal.user3.institution 9id:bss:proposal:user3:institution
+proposal.user3.institution_id 9id:bss:proposal:user3:instId
+proposal.user3.last_name 9id:bss:proposal:user3:lastName
+proposal.user3.pi_flag 9id:bss:proposal:user3:piFlag OFF
+proposal.user3.user_id 9id:bss:proposal:user3:userId
+proposal.user4.badge_number 9id:bss:proposal:user4:badgeNumber
+proposal.user4.email 9id:bss:proposal:user4:email
+proposal.user4.first_name 9id:bss:proposal:user4:firstName
+proposal.user4.institution 9id:bss:proposal:user4:institution
+proposal.user4.institution_id 9id:bss:proposal:user4:instId
+proposal.user4.last_name 9id:bss:proposal:user4:lastName
+proposal.user4.pi_flag 9id:bss:proposal:user4:piFlag OFF
+proposal.user4.user_id 9id:bss:proposal:user4:userId
+proposal.user5.badge_number 9id:bss:proposal:user5:badgeNumber
+proposal.user5.email 9id:bss:proposal:user5:email
+proposal.user5.first_name 9id:bss:proposal:user5:firstName
+proposal.user5.institution 9id:bss:proposal:user5:institution
+proposal.user5.institution_id 9id:bss:proposal:user5:instId
+proposal.user5.last_name 9id:bss:proposal:user5:lastName
+proposal.user5.pi_flag 9id:bss:proposal:user5:piFlag OFF
+proposal.user5.user_id 9id:bss:proposal:user5:userId
+proposal.user6.badge_number 9id:bss:proposal:user6:badgeNumber
+proposal.user6.email 9id:bss:proposal:user6:email
+proposal.user6.first_name 9id:bss:proposal:user6:firstName
+proposal.user6.institution 9id:bss:proposal:user6:institution
+proposal.user6.institution_id 9id:bss:proposal:user6:instId
+proposal.user6.last_name 9id:bss:proposal:user6:lastName
+proposal.user6.pi_flag 9id:bss:proposal:user6:piFlag OFF
+proposal.user6.user_id 9id:bss:proposal:user6:userId
+proposal.user7.badge_number 9id:bss:proposal:user7:badgeNumber
+proposal.user7.email 9id:bss:proposal:user7:email
+proposal.user7.first_name 9id:bss:proposal:user7:firstName
+proposal.user7.institution 9id:bss:proposal:user7:institution
+proposal.user7.institution_id 9id:bss:proposal:user7:instId
+proposal.user7.last_name 9id:bss:proposal:user7:lastName
+proposal.user7.pi_flag 9id:bss:proposal:user7:piFlag OFF
+proposal.user7.user_id 9id:bss:proposal:user7:userId
+proposal.user8.badge_number 9id:bss:proposal:user8:badgeNumber
+proposal.user8.email 9id:bss:proposal:user8:email
+proposal.user8.first_name 9id:bss:proposal:user8:firstName
+proposal.user8.institution 9id:bss:proposal:user8:institution
+proposal.user8.institution_id 9id:bss:proposal:user8:instId
+proposal.user8.last_name 9id:bss:proposal:user8:lastName
+proposal.user8.pi_flag 9id:bss:proposal:user8:piFlag OFF
+proposal.user8.user_id 9id:bss:proposal:user8:userId
+proposal.user9.badge_number 9id:bss:proposal:user9:badgeNumber
+proposal.user9.email 9id:bss:proposal:user9:email
+proposal.user9.first_name 9id:bss:proposal:user9:firstName
+proposal.user9.institution 9id:bss:proposal:user9:institution
+proposal.user9.institution_id 9id:bss:proposal:user9:instId
+proposal.user9.last_name 9id:bss:proposal:user9:lastName
+proposal.user9.pi_flag 9id:bss:proposal:user9:piFlag OFF
+proposal.user9.user_id 9id:bss:proposal:user9:userId
+proposal.user_badges 9id:bss:proposal:userBadges 86312,85283
+proposal.user_last_names 9id:bss:proposal:users Ilavsky,Okasinski
+============================= ================================== ==============================
diff --git a/docs/source/resources/bsv1.jpg b/docs/source/resources/bsv1.jpg
old mode 100755
new mode 100644
diff --git a/docs/source/resources/excel_plan_spreadsheet.jpg b/docs/source/resources/excel_plan_spreadsheet.jpg
old mode 100755
new mode 100644
diff --git a/docs/source/resources/excel_simple.jpg b/docs/source/resources/excel_simple.jpg
old mode 100755
new mode 100644
diff --git a/docs/source/resources/sample_example.jpg b/docs/source/resources/sample_example.jpg
old mode 100755
new mode 100644
diff --git a/docs/source/resources/sample_example.xlsx b/docs/source/resources/sample_example.xlsx
old mode 100755
new mode 100644
diff --git a/docs/source/resources/ui_id_entered.png b/docs/source/resources/ui_id_entered.png
new file mode 100644
index 000000000..c4e5e724b
Binary files /dev/null and b/docs/source/resources/ui_id_entered.png differ
diff --git a/docs/source/resources/ui_initialized.png b/docs/source/resources/ui_initialized.png
new file mode 100644
index 000000000..8bda52dc6
Binary files /dev/null and b/docs/source/resources/ui_initialized.png differ
diff --git a/docs/source/resources/ui_updated.png b/docs/source/resources/ui_updated.png
new file mode 100644
index 000000000..8d43f8db3
Binary files /dev/null and b/docs/source/resources/ui_updated.png differ
diff --git a/docs/source/source/beamtime/_apsbss makedb.rst b/docs/source/source/beamtime/_apsbss makedb.rst
new file mode 100644
index 000000000..014bd50d1
--- /dev/null
+++ b/docs/source/source/beamtime/_apsbss makedb.rst
@@ -0,0 +1,7 @@
+
+apsbss: Create EPICS database
+-----------------------------
+
+.. automodule:: apstools.beamtime.apsbss_makedb
+ :members:
+
diff --git a/docs/source/source/beamtime/_apsbss.rst b/docs/source/source/beamtime/_apsbss.rst
new file mode 100644
index 000000000..fea8495cc
--- /dev/null
+++ b/docs/source/source/beamtime/_apsbss.rst
@@ -0,0 +1,7 @@
+
+apsbss: Command line application
+--------------------------------
+
+.. automodule:: apstools.beamtime.apsbss
+ :members:
+
diff --git a/docs/source/source/beamtime/_apsbss_ophyd.rst b/docs/source/source/beamtime/_apsbss_ophyd.rst
new file mode 100644
index 000000000..df97ab423
--- /dev/null
+++ b/docs/source/source/beamtime/_apsbss_ophyd.rst
@@ -0,0 +1,7 @@
+
+apsbss: Ophyd Device
+--------------------
+
+.. automodule:: apstools.beamtime.apsbss_ophyd
+ :members:
+
diff --git a/docs/source/source/beamtime/index.rst b/docs/source/source/beamtime/index.rst
new file mode 100644
index 000000000..784b74b92
--- /dev/null
+++ b/docs/source/source/beamtime/index.rst
@@ -0,0 +1,14 @@
+.. _beamtime_source_docs:
+
+APS Proposal and ESAF Support
+=============================
+
+Source code documentation of the
+support for the APS Proposal and ESAF
+(experiment safety approval form) databases.
+
+.. toctree::
+ :maxdepth: 2
+ :glob:
+
+ _*
diff --git a/docs/source/source/index.rst b/docs/source/source/index.rst
index ffdf3e690..48e0d254c 100644
--- a/docs/source/source/index.rst
+++ b/docs/source/source/index.rst
@@ -8,4 +8,5 @@ API Documentation
:glob:
_*
+ beamtime/index
synApps/index
diff --git a/environment.yml b/environment.yml
index df599f443..9ea47f937 100644
--- a/environment.yml
+++ b/environment.yml
@@ -1,19 +1,27 @@
name: apstoolsdoc
channels:
-- nsls2forge
-- defaults
+ - defaults
+ - conda-forge
+ - aps-anl-tag
+ - nsls2forge
dependencies:
-- python=3.6
-- qt=5
-- pyqt=5
-- sphinx
-- sphinx_rtd_theme
-- bluesky
-- ophyd
-- prettytable
-- pip:
- - ipython-genutils==0.2.0
- - prompt-toolkit==1.0.15
- - sphinx-rtd-theme
- - super-state-machine
- - versioneer
+ - python>=3.6.0
+ - aps-dm-api
+ - bluesky>=1.6.2
+ - databroker>=1.0.6
+ - h5py
+ - ophyd>=1.5.1
+ - pip
+ - pyEpics>=3.4.2
+ - pyqt=5
+ - pyRestTable
+ - qt=5
+ - spec2nexus>=2021.1.7
+ - sphinx
+ - xlrd
+ - pip:
+ - ipython-genutils==0.2.0
+ - prompt-toolkit==1.0.15
+ - sphinx-rtd-theme
+ - super-state-machine
+ - versioneer
diff --git a/setup.py b/setup.py
index 810d1576e..f5541e1e7 100755
--- a/setup.py
+++ b/setup.py
@@ -1,5 +1,5 @@
"""
-packaging setup for apstools
+Packaging setup for apstools.
"""
# Always prefer setuptools over distutils
@@ -16,6 +16,7 @@
__entry_points__ = {
'console_scripts': [
+ 'apsbss = apstools.beamtime.apsbss:main',
'apstools_plan_catalog = apstools.examples:main',
'bluesky_snapshot = apstools.snapshot:snapshot_cli',
'bluesky_snapshot_viewer = apstools.snapshot:snapshot_gui',
diff --git a/tests/__main__.py b/tests/__main__.py
index 154cd4429..65406de17 100644
--- a/tests/__main__.py
+++ b/tests/__main__.py
@@ -12,6 +12,7 @@
def suite(*args, **kw):
+ from tests import test_beamtime
from tests import test_simple
from tests import test_filewriter
from tests import test_export_json
@@ -25,6 +26,7 @@ def suite(*args, **kw):
test_exceltable,
test_commandlist,
test_utils,
+ test_beamtime,
]
test_suite = unittest.TestSuite()
diff --git a/tests/test_beamtime.py b/tests/test_beamtime.py
new file mode 100644
index 000000000..5a9a9a7ae
--- /dev/null
+++ b/tests/test_beamtime.py
@@ -0,0 +1,324 @@
+
+"""
+unit tests for beamtime info
+"""
+
+import datetime
+import os
+import socket
+import subprocess
+import sys
+import time
+import unittest
+import uuid
+
+_test_path = os.path.dirname(__file__)
+_path = os.path.join(_test_path, '..')
+if _path not in sys.path:
+ sys.path.insert(0, _path)
+
+from apstools.beamtime import apsbss, apsbss_makedb
+
+BSS_TEST_IOC_PREFIX = f"tst{uuid.uuid4().hex[:7]}:bss:"
+
+
+def using_APS_workstation():
+ hostname = socket.gethostname()
+ return hostname.lower().endswith(".aps.anl.gov")
+
+
+def bss_IOC_available():
+ import epics
+ # try connecting with one of the PVs in the database
+ cycle = epics.PV(f"{BSS_TEST_IOC_PREFIX}esaf:cycle")
+ cycle.wait_for_connection(timeout=2)
+ return cycle.connected
+
+
+class Test_Beamtime(unittest.TestCase):
+
+ def test_general(self):
+ self.assertEqual(apsbss.CONNECT_TIMEOUT, 5)
+ self.assertEqual(apsbss.POLL_INTERVAL, 0.01)
+ self.assertEqual(
+ apsbss.DM_APS_DB_WEB_SERVICE_URL,
+ "https://xraydtn01.xray.aps.anl.gov:11236")
+ self.assertIsNotNone(apsbss.api_bss)
+ self.assertIsNotNone(apsbss.api_esaf)
+
+ def test_iso2datetime(self):
+ self.assertEqual(
+ apsbss.iso2datetime("2020-06-30 12:31:45.067890"),
+ datetime.datetime(2020, 6, 30, 12, 31, 45, 67890)
+ )
+
+ def test_not_at_aps(self):
+ self.assertTrue(True) # test something
+ if using_APS_workstation():
+ return
+
+ # do not try to test for fails using dm package, it has no timeout
+
+ def test_only_at_aps(self):
+ self.assertTrue(True) # test something
+ if not using_APS_workstation():
+ return
+
+ runs = apsbss.listAllRuns()
+ self.assertGreater(len(runs), 1)
+ self.assertEqual(apsbss.getCurrentCycle(), runs[-1])
+ self.assertEqual(apsbss.listRecentRuns()[0], runs[-1])
+
+ self.assertGreater(len(apsbss.listAllBeamlines()), 1)
+
+ # TODO: test the other functions
+ # getCurrentEsafs
+ # getCurrentInfo
+ # getCurrentProposals
+ # getEsaf
+ # getProposal
+ # class DmRecordNotFound(Exception): ...
+ # class EsafNotFound(DmRecordNotFound): ...
+ # class ProposalNotFound(DmRecordNotFound): ...
+
+ def test_printColumns(self):
+ from tests.common import Capture_stdout
+ with Capture_stdout() as received:
+ apsbss.printColumns("1 2 3 4 5 6".split(), numColumns=3, width=3)
+ self.assertEqual(len(received), 2)
+ self.assertEqual(received[0], "1 3 5 ")
+ self.assertEqual(received[1], "2 4 6 ")
+
+ source = "0123456789"
+ self.assertEqual(apsbss.trim(source), source)
+ got = apsbss.trim(source, length=8)
+ self.assertNotEqual(got, source)
+ self.assertTrue(got.endswith("..."))
+ self.assertEqual(len(got), 8)
+ self.assertEqual(got, "01234...")
+
+
+class Test_EPICS(unittest.TestCase):
+
+ def setUp(self):
+ self.bss = None
+ self.manager = os.path.abspath(os.path.join(
+ os.path.dirname(apsbss.__file__),
+ "apsbss_ioc.sh"
+ ))
+ self.ioc_name = "test_apsbss"
+ cmd = f"{self.manager} restart {self.ioc_name} {BSS_TEST_IOC_PREFIX}"
+ self.ioc_process = subprocess.Popen(
+ cmd.encode().split(),
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ shell=False)
+ time.sleep(.5) # allow the IOC to start
+
+ def tearDown(self):
+ if self.bss is not None:
+ self.bss.destroy()
+ self.bss = None
+ if self.ioc_process is not None:
+ self.ioc_process = None
+ cmd = f"{self.manager} stop {self.ioc_name} {BSS_TEST_IOC_PREFIX}"
+ subprocess.Popen(
+ cmd.encode().split(),
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ shell=False)
+ # self.ioc_process.communicate(cmd.encode().split())
+ self.manager = None
+
+ def test_ioc(self):
+ if not bss_IOC_available():
+ return
+
+ from apstools.beamtime import apsbss_ophyd as bio
+
+ self.bss = bio.EpicsBssDevice(BSS_TEST_IOC_PREFIX, name="bss")
+ self.bss.wait_for_connection(timeout=2)
+ self.assertTrue(self.bss.connected)
+ self.assertEqual(self.bss.esaf.title.get(), "")
+ self.assertEqual(self.bss.esaf.description.get(), "")
+ # self.assertEqual(self.bss.esaf.aps_cycle.get(), "")
+
+ def test_EPICS(self):
+ from tests.common import Capture_stdout
+
+ if not bss_IOC_available():
+ return
+
+ beamline = "9-ID-B,C"
+ cycle = "2019-3"
+
+ with Capture_stdout():
+ self.bss = apsbss.connect_epics(BSS_TEST_IOC_PREFIX)
+ self.assertTrue(self.bss.connected)
+ self.assertEqual(self.bss.esaf.aps_cycle.get(), "")
+
+ self.bss.esaf.aps_cycle.put(cycle)
+ self.assertNotEqual(self.bss.esaf.aps_cycle.get(), "")
+
+ if not using_APS_workstation():
+ return
+
+ # setup
+ with Capture_stdout():
+ apsbss.epicsSetup(BSS_TEST_IOC_PREFIX, beamline, cycle)
+ self.assertNotEqual(self.bss.proposal.beamline_name.get(), "harpo")
+ self.assertEqual(self.bss.proposal.beamline_name.get(), beamline)
+ self.assertEqual(self.bss.esaf.aps_cycle.get(), cycle)
+ self.assertEqual(self.bss.esaf.sector.get(), beamline.split("-")[0])
+
+ # epicsUpdate
+ # Example ESAF on sector 9
+
+ # ====== ======== ========== ========== ==================== =================================
+ # id status start end user(s) title
+ # ====== ======== ========== ========== ==================== =================================
+ # 226319 Approved 2020-05-26 2020-09-28 Ilavsky,Maxey,Kuz... Commission 9ID and USAXS
+ # ====== ======== ========== ========== ==================== =================================
+
+ # ===== ====== =================== ==================== ========================================
+ # id cycle date user(s) title
+ # ===== ====== =================== ==================== ========================================
+ # 64629 2019-2 2019-03-01 18:35:02 Ilavsky,Okasinski 2019 National School on Neutron & X-r...
+ # ===== ====== =================== ==================== ========================================
+ esaf_id = "226319"
+ proposal_id = "64629"
+ self.bss.esaf.aps_cycle.put("2019-2")
+ self.bss.esaf.esaf_id.put(esaf_id)
+ self.bss.proposal.proposal_id.put(proposal_id)
+ with Capture_stdout():
+ apsbss.epicsUpdate(BSS_TEST_IOC_PREFIX)
+ self.assertEqual(
+ self.bss.esaf.title.get(),
+ "Commission 9ID and USAXS")
+ self.assertTrue(
+ self.bss.proposal.title.get().startswith(
+ "2019 National School on Neutron & X-r"))
+
+ with Capture_stdout():
+ apsbss.epicsClear(BSS_TEST_IOC_PREFIX)
+ self.assertNotEqual(self.bss.esaf.aps_cycle.get(), "")
+ self.assertEqual(self.bss.esaf.title.get(), "")
+ self.assertEqual(self.bss.proposal.title.get(), "")
+
+
+class Test_MakeDatabase(unittest.TestCase):
+
+ def test_general(self):
+ from tests.common import Capture_stdout
+ with Capture_stdout() as db:
+ apsbss_makedb.main()
+ self.assertEqual(len(db), 345)
+ self.assertEqual(db[0], "#")
+ self.assertEqual(db[1], "# file: apsbss.db")
+ # randomly-selected spot checks
+ self.assertEqual(db[22], 'record(stringout, "$(P)esaf:id")')
+ self.assertEqual(db[128], ' field(ONAM, "ON")')
+ self.assertEqual(db[265], ' field(FTVL, "CHAR")')
+
+
+class Test_ProgramCommands(unittest.TestCase):
+
+ def setUp(self):
+ self.sys_argv0 = sys.argv[0]
+ sys.argv = [self.sys_argv0,]
+
+ def tearDown(self):
+ sys.argv = [self.sys_argv0,]
+
+ def test_no_options(self):
+ args = apsbss.get_options()
+ self.assertIsNotNone(args)
+ self.assertIsNone(args.subcommand)
+
+ def test_beamlines(self):
+ sys.argv.append("beamlines")
+ args = apsbss.get_options()
+ self.assertIsNotNone(args)
+ self.assertEqual(args.subcommand, "beamlines")
+
+ def test_current(self):
+ sys.argv.append("current")
+ sys.argv.append("9-ID-B,C")
+ args = apsbss.get_options()
+ self.assertIsNotNone(args)
+ self.assertEqual(args.subcommand, "current")
+ self.assertEqual(args.beamlineName, "9-ID-B,C")
+
+ def test_cycles(self):
+ sys.argv.append("cycles")
+ args = apsbss.get_options()
+ self.assertIsNotNone(args)
+ self.assertEqual(args.subcommand, "cycles")
+
+ def test_esaf(self):
+ sys.argv.append("esaf")
+ sys.argv.append("12345")
+ args = apsbss.get_options()
+ self.assertIsNotNone(args)
+ self.assertEqual(args.subcommand, "esaf")
+ self.assertEqual(args.esafId, 12345)
+
+ def test_proposal(self):
+ sys.argv.append("proposal")
+ sys.argv.append("proposal_number_here")
+ sys.argv.append("1995-1")
+ sys.argv.append("my_beamline")
+ args = apsbss.get_options()
+ self.assertIsNotNone(args)
+ self.assertEqual(args.subcommand, "proposal")
+ self.assertEqual(args.proposalId, "proposal_number_here")
+ self.assertEqual(args.cycle, "1995-1")
+ self.assertEqual(args.beamlineName, "my_beamline")
+
+ def test_EPICS_clear(self):
+ sys.argv.append("clear")
+ sys.argv.append("bss:")
+ args = apsbss.get_options()
+ self.assertIsNotNone(args)
+ self.assertEqual(args.subcommand, "clear")
+ self.assertEqual(args.prefix, "bss:")
+
+ def test_EPICS_setup(self):
+ sys.argv.append("setup")
+ sys.argv.append("bss:")
+ sys.argv.append("my_beamline")
+ sys.argv.append("1995-1")
+ args = apsbss.get_options()
+ self.assertIsNotNone(args)
+ self.assertEqual(args.subcommand, "setup")
+ self.assertEqual(args.prefix, "bss:")
+ self.assertEqual(args.beamlineName, "my_beamline")
+ self.assertEqual(args.cycle, "1995-1")
+
+ def test_EPICS_update(self):
+ sys.argv.append("update")
+ sys.argv.append("bss:")
+ args = apsbss.get_options()
+ self.assertIsNotNone(args)
+ self.assertEqual(args.subcommand, "update")
+ self.assertEqual(args.prefix, "bss:")
+
+
+def suite(*args, **kw):
+ test_list = [
+ Test_Beamtime,
+ Test_EPICS,
+ Test_MakeDatabase,
+ Test_ProgramCommands,
+ ]
+ test_suite = unittest.TestSuite()
+ for test_case in test_list:
+ test_suite.addTest(unittest.makeSuite(test_case))
+ return test_suite
+
+
+if __name__ == "__main__":
+ runner=unittest.TextTestRunner()
+ runner.run(suite())
diff --git a/tests/test_filewriter.py b/tests/test_filewriter.py
index bff08d121..9170573bd 100644
--- a/tests/test_filewriter.py
+++ b/tests/test_filewriter.py
@@ -242,7 +242,7 @@ def test_receiver_battery(self):
callback.file_extension,
apstools.filewriters.NEXUS_FILE_EXTENSION)
- for plan_name, document_set in self.db.items():
+ for plan_name in self.db:
callback.clear()
callback.file_path = self.tempdir
self.assertIsNone(callback.uid, plan_name)