├── .gitattributes ├── .gitignore ├── LAE500 └── lae500.py ├── LICENSE ├── Legacy ├── ExampleApp │ ├── Example.db │ ├── ExampleApp.py │ ├── PVLIST_Example.TXT │ ├── example.adl │ ├── py_exapp.db │ ├── py_exapp.req │ ├── st_cmd.txt │ └── start_app ├── IonChamber │ ├── Mucal.py │ ├── check_ionchamber.py │ └── start_ionchamber.py ├── MCADisplay │ ├── base │ │ └── __init__.py │ ├── lib │ │ ├── __init__.py │ │ └── mcadisplay.py │ ├── pyepics_mcadisplay.py │ ├── setup.py │ └── setup_py2app.py ├── MotorSetup │ ├── base │ │ └── __init__.py │ ├── lib │ │ ├── __init__.py │ │ ├── configfile.py │ │ ├── create_init_motordb.py │ │ ├── motor_setup.py │ │ ├── motorapp_utils.py │ │ ├── motordb.py │ │ ├── mysql_settings.py │ │ ├── new_mysql_db.py │ │ └── utils.py │ ├── pyepics_motorsetup.py │ └── setup.py └── XRFCollector │ ├── MED_Collect.py │ ├── XRF_Collect.adl │ ├── XRM_XMAP_PVS.DAT │ ├── iocboot │ ├── .svn │ │ ├── all-wcprops │ │ ├── entries │ │ └── text-base │ │ │ ├── XRF_Collect.db.svn-base │ │ │ ├── XRF_Collect.req.svn-base │ │ │ └── st_cmd_text.svn-base │ ├── XRF_Collect.db │ ├── XRF_Collect.req │ └── st.cmd │ └── start_xrfcollect ├── MANIFEST.in ├── README.md ├── README.txt ├── doc ├── Makefile ├── _templates │ ├── indexsidebar.html │ └── layout.html ├── ad_display.rst ├── conf.py ├── images │ ├── AD_Display.png │ ├── InstMain_DXP.png │ ├── InstMain_Mixed.png │ ├── InstMain_Stage.png │ ├── Inst_Edit.png │ ├── Inst_GoTo.png │ ├── Inst_MEDM.png │ ├── Inst_Settings.png │ ├── Inst_Startup.png │ ├── MotorSetup.png │ ├── pvlogger_eventval.png │ ├── pvlogger_mainview.png │ ├── pvlogger_plotenum.png │ ├── pvlogger_plotone.png │ ├── pvlogger_plotsel.png │ ├── pvlogger_table.png │ └── stripchart.png ├── index.rst ├── installation.rst ├── instruments.rst ├── other.rst ├── publish_docs.sh ├── pvlogger.rst └── stripchart.rst ├── epicsapps ├── __init__.py ├── apps.py ├── areadetector │ ├── __init__.py │ ├── ad_config.py │ ├── ad_display.py │ ├── ad_scandbcallback.py │ ├── calibration_dialog.py │ ├── contrast_control.py │ ├── debugtime.py │ ├── icons │ │ └── __init__.py │ ├── imagepanel.py │ ├── imageview.py │ ├── pvconfig.py │ └── xrd_integrator.py ├── icons │ ├── areadetector.icns │ ├── areadetector.ico │ ├── camera.icns │ ├── camera.ico │ ├── instrument.icns │ ├── instrument.ico │ ├── logging.icns │ ├── logging.ico │ ├── microscope.icns │ ├── microscope.ico │ ├── motorapp.ico │ ├── stripchart.icns │ └── stripchart.ico ├── instruments │ ├── InstrumentApp.py │ ├── __init__.py │ ├── configfile.py │ ├── creator.py │ ├── editframe.py │ ├── epics_server.py │ ├── instrument.py │ ├── instrumentpanel.py │ ├── pvconnector.py │ ├── settingsframe.py │ ├── simpledb.py │ ├── test.py │ ├── upgrades.py │ └── utils.py ├── ionchamber │ ├── __init__.py │ ├── iocApp │ │ ├── IonChamber.adl │ │ ├── IonChamber.db │ │ ├── IonChamber.req │ │ └── st.cmd │ └── ionchamber.py ├── microscope │ ├── __init__.py │ ├── calibrationframe.py │ ├── configfile.py │ ├── controlpanel.py │ ├── fly2_camera.py │ ├── html2pos.py │ ├── icons.py │ ├── imageframe.py │ ├── imagepanel_base.py │ ├── imagepanel_epicsAD.py │ ├── imagepanel_epicsarray.py │ ├── imagepanel_fly2.py │ ├── imagepanel_pyspin.py │ ├── imagepanel_weburl.py │ ├── imagepanel_zmqjpeg.py │ ├── microscope.py │ ├── motorpanel.py │ ├── overlayframe.py │ ├── positionpanel.py │ ├── pyspin_camera.py │ └── transformations.py ├── pvlogger │ ├── __init__.py │ ├── configfile.py │ ├── eventtableview.py │ ├── logfile.py │ ├── plotter.py │ ├── pvlogger.py │ ├── pvlogger_app.py │ └── pvtableview.py ├── stripchart │ ├── __init__.py │ ├── configfile.py │ └── stripchart.py └── utils │ ├── __init__.py │ ├── configfile.py │ ├── debugtimer.py │ ├── griddata.py │ ├── moveto_dialog.py │ ├── textfile.py │ ├── utils.py │ └── wxutils.py ├── examples ├── README.md ├── areaDetector │ ├── ID13_Eiger500K.yaml │ ├── ID13_SecondarySourceAperture.yaml │ ├── ID13_foe_camera.yaml │ └── eiger500k.ico ├── epics_scan │ ├── escan_credentials.dat │ └── escan_pgschema.sql ├── instruments │ ├── epics_client │ │ ├── PyInstrument.adl │ │ ├── PyInstrument.db │ │ ├── epics_inst.pro │ │ └── epics_instrument__define.pro │ ├── example_sqlite.ein │ ├── instruments.yaml │ └── sqlite2pg.py └── microscope │ └── microscope.yaml ├── pyproject.toml ├── requirements.txt ├── setup.py └── tt.yaml /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | epicsapps/_version.py export-subst 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *~ 3 | *# 4 | *.bak 5 | doc/_build 6 | doc/*.pdf 7 | tests/.coverage* 8 | build 9 | dist 10 | junk 11 | sandbox 12 | epicsapps/version.py 13 | epicsapps.egg-info/top_level.txt 14 | epicsapps.egg-info/dependency_links.txt 15 | epicsapps.egg-info/PKG-INFO 16 | epicsapps.egg-info/entry_points.txt 17 | epicsapps.egg-info/requires.txt 18 | epicsapps.egg-info/SOURCES.txt 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 Matthew Newville, The University of Chicago. All rights reserved. 2 | 3 | The epics python module is distributed subject to the following license conditions: 4 | SOFTWARE LICENSE AGREEMENT 5 | Software: epics python module 6 | 7 | 1. The "Software", below, refers to the epics python module (in either 8 | source code, or binary form and accompanying documentation). Each 9 | licensee is addressed as "you" or "Licensee." 10 | 11 | 2. The copyright holders shown above and their third-party licensors 12 | hereby grant Licensee a royalty-free nonexclusive license, subject to 13 | the limitations stated herein and U.S. Government license rights. 14 | 15 | 3. You may modify and make a copy or copies of the Software for use 16 | within your organization, if you meet the following conditions: 17 | 18 | 1. Copies in source code must include the copyright notice and this 19 | Software License Agreement. 20 | 21 | 2. Copies in binary form must include the copyright notice and this 22 | Software License Agreement in the documentation and/or other 23 | materials provided with the copy. 24 | 25 | 4. You may modify a copy or copies of the Software or any portion of 26 | it, thus forming a work based on the Software, and distribute copies of 27 | such work outside your organization, if you meet all of the following 28 | conditions: 29 | 30 | 1. Copies in source code must include the copyright notice and this 31 | Software License Agreement; 32 | 33 | 2. Copies in binary form must include the copyright notice and this 34 | Software License Agreement in the documentation and/or other 35 | materials provided with the copy; 36 | 37 | 3. Modified copies and works based on the Software must carry 38 | prominent notices stating that you changed specified portions of 39 | the Software. 40 | 41 | 5. Portions of the Software resulted from work developed under a 42 | U.S. Government contract and are subject to the following license: the 43 | Government is granted for itself and others acting on its behalf a 44 | paid-up, nonexclusive, irrevocable worldwide license in this computer 45 | software to reproduce, prepare derivative works, and perform publicly 46 | and display publicly. 47 | 48 | 6. WARRANTY DISCLAIMER. THE SOFTWARE IS SUPPLIED "AS IS" WITHOUT 49 | WARRANTY OF ANY KIND. THE COPYRIGHT HOLDERS, THEIR THIRD PARTY 50 | LICENSORS, THE UNITED STATES, THE UNITED STATES DEPARTMENT OF ENERGY, 51 | AND THEIR EMPLOYEES: (1) DISCLAIM ANY WARRANTIES, EXPRESS OR IMPLIED, 52 | INCLUDING BUT NOT LIMITED TO ANY IMPLIED WARRANTIES OF MERCHANTABILITY, 53 | FITNESS FOR A PARTICULAR PURPOSE, TITLE OR NON-INFRINGEMENT, (2) DO NOT 54 | ASSUME ANY LEGAL LIABILITY OR RESPONSIBILITY FOR THE ACCURACY, 55 | COMPLETENESS, OR USEFULNESS OF THE SOFTWARE, (3) DO NOT REPRESENT THAT 56 | USE OF THE SOFTWARE WOULD NOT INFRINGE PRIVATELY OWNED RIGHTS, (4) DO 57 | NOT WARRANT THAT THE SOFTWARE WILL FUNCTION UNINTERRUPTED, THAT IT IS 58 | ERROR-FREE OR THAT ANY ERRORS WILL BE CORRECTED. 59 | 60 | 7. LIMITATION OF LIABILITY. IN NO EVENT WILL THE COPYRIGHT HOLDERS, 61 | THEIR THIRD PARTY LICENSORS, THE UNITED STATES, THE UNITED STATES 62 | DEPARTMENT OF ENERGY, OR THEIR EMPLOYEES: BE LIABLE FOR ANY INDIRECT, 63 | INCIDENTAL, CONSEQUENTIAL, SPECIAL OR PUNITIVE DAMAGES OF ANY KIND OR 64 | NATURE, INCLUDING BUT NOT LIMITED TO LOSS OF PROFITS OR LOSS OF DATA, 65 | FOR ANY REASON WHATSOEVER, WHETHER SUCH LIABILITY IS ASSERTED ON THE 66 | BASIS OF CONTRACT, TORT (INCLUDING NEGLIGENCE OR STRICT LIABILITY), OR 67 | OTHERWISE, EVEN IF ANY OF SAID PARTIES HAS BEEN WARNED OF THE 68 | POSSIBILITY OF SUCH LOSS OR DAMAGES. 69 | 70 | ------------------------------------------------ 71 | -------------------------------------------------------------------------------- /Legacy/ExampleApp/Example.db: -------------------------------------------------------------------------------- 1 | # This database contains fields used to allow 2 | # communication between XRF_Collector and clients 3 | 4 | record(mbbo,"$(P)$(Q):status") { 5 | field(DESC,"Read State") 6 | field(VAL, "0") 7 | field(ZRVL,"0") 8 | field(ZRST,"Done") 9 | field(ONVL,"1") 10 | field(ONST,"Collecting") 11 | field(TWVL,"2") 12 | field(TWST,"Writing") 13 | field(THVL,"3") 14 | field(THST,"Not Connected") 15 | } 16 | 17 | record(mbbo,"$(P)$(Q):mode") { 18 | field(DESC,"Collection Mode") 19 | field(VAL, "0") 20 | field(ZRVL,"0") 21 | field(ZRST,"Empty") 22 | field(ONVL,"1") 23 | field(ONST,"Background") 24 | field(TWVL,"2") 25 | field(TWST,"Data") 26 | field(THVL,"3") 27 | field(THST,"Disconnected") 28 | 29 | } 30 | 31 | record(ao,"$(P)$(Q):counttime") { 32 | field(DESC,"Collection Time") 33 | field(PREC,"2") 34 | field(VAL,"1.0") 35 | } 36 | 37 | record(mbbo,"$(P)$(Q):request") { 38 | field(DESC,"Client Requests Collection") 39 | field(VAL, "0") 40 | field(ZRVL,"0") 41 | field(ZRST,"Stop") 42 | field(ONVL,"1") 43 | field(ONST,"Start") 44 | } 45 | 46 | record(waveform,"$(P)$(Q):host") { 47 | field(DTYP,"Soft Channel") 48 | field(NELM,"128") 49 | field(FTVL,"CHAR") 50 | field(DESC, "file host") 51 | } 52 | 53 | record(waveform,"$(P)$(Q):folder") { 54 | field(DTYP,"Soft Channel") 55 | field(DESC, "file directory") 56 | field(NELM,"128") 57 | field(FTVL,"CHAR") 58 | } 59 | 60 | record(waveform,"$(P)$(Q):filename") { 61 | field(DTYP,"Soft Channel") 62 | field(DESC, "filename") 63 | field(VAL, "") 64 | field(NELM,"128") 65 | field(FTVL,"CHAR") 66 | } 67 | 68 | record(stringin,"$(P)$(Q):fileext") { 69 | field(DTYP,"Soft Channel") 70 | field(DESC, "filename") 71 | field(VAL, "") 72 | field(NELM,"128") 73 | field(FTVL,"CHAR") 74 | } 75 | 76 | 77 | record(stringin, "$(P)$(Q):fileext") { 78 | field(DTYP,"Soft Channel") 79 | field(DESC, "file extesion") 80 | field(VAL, "001") 81 | } 82 | xo 83 | record(stringin,"$(P)$(Q):format") { 84 | field(DTYP,"Soft Channel") 85 | field(DESC, "file format string") 86 | field(VAL, "%s.%s") 87 | } 88 | 89 | 90 | record(waveform,"$(P)$(Q):message") { 91 | field(DTYP,"Soft Channel") 92 | field(DESC,"status message") 93 | field(NELM,"128") 94 | field(FTVL,"CHAR") 95 | } 96 | 97 | 98 | record(stringin,"$(P)$(Q):timestamp") { 99 | field(DTYP,"Soft Channel") 100 | field(DESC, "timestamp as string") 101 | field(VAL, "Starting") 102 | } 103 | 104 | record(longout,"$(P)$(Q):unixtime") { 105 | field(DTYP,"Soft Channel") 106 | field(DESC, "timestamp as integer") 107 | field(VAL, 0) 108 | } 109 | 110 | record(waveform,"$(P)$(Q):arg1") { 111 | field(DTYP,"Soft Channel") 112 | field(DESC,"user arg 1") 113 | field(NELM,"128") 114 | field(FTVL,"CHAR") 115 | } 116 | 117 | record(waveform,"$(P)$(Q):arg2") { 118 | field(DTYP,"Soft Channel") 119 | field(DESC,"user arg 2") 120 | field(NELM,"128") 121 | field(FTVL,"CHAR") 122 | } 123 | 124 | record(waveform,"$(P)$(Q):arg3") { 125 | field(DTYP,"Soft Channel") 126 | field(DESC,"user arg 3") 127 | field(NELM,"128") 128 | field(FTVL,"CHAR") 129 | } 130 | -------------------------------------------------------------------------------- /Legacy/ExampleApp/ExampleApp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | import os 5 | import time 6 | from epics import PV, Device 7 | 8 | burt_command0 = 'burt -l /somefile' 9 | burt_command0 = 'ls -l' 10 | 11 | PVFILE1 = 'PVLIST.TXT' 12 | 13 | class Collector: 14 | attrs = ('status', 'mode', 'request', 15 | 'host', 'folder', 'filename', 'fileext', 'format', 16 | 'message', 'timestamp', 'unixtime', 17 | 'arg1', 'arg2', 'arg3', 'counttime') 18 | 19 | def __init__(self, prefix): 20 | self.device = Device(prefix, delim=':', attrs=self.attrs) 21 | 22 | time.sleep(0.1) 23 | self.read_pvfile() 24 | 25 | # self.device.add_callback('request', self.onRequest) 26 | def onRequest(self, pvname=None, value=None, **kws): 27 | print 'Request changed to ', value 28 | 29 | def read_pvfile(self): 30 | try: 31 | f = open(PVFILE1, 'r') 32 | lines = f.readlines() 33 | f.close() 34 | except: 35 | self.env_pvs = None 36 | return 37 | self.env_pvs = [] 38 | for i in lines: 39 | i = i[:-1].strip() 40 | if len(i)<2: continue 41 | words = i.split() 42 | pvname = words.pop(0) 43 | desc = ' '.join(words).strip() 44 | pv = PV(pvname) 45 | pv.get() 46 | if pv: 47 | if desc == '': desc = pv.desc 48 | self.env_pvs.append( (pv,desc) ) 49 | print 'Will use %i PVs from %s' % (len(self.env_pvs), PVFILE1) 50 | 51 | 52 | def setTime(self, ts=None): 53 | if ts is None: 54 | ts = time.time() 55 | self.device.timestamp = time.ctime(ts) # self.device.timestamp = Py:EXT:timestamp 56 | self.device.unixtime = ts 57 | 58 | def setMessage(self, msg): 59 | self.device.message = msg 60 | 61 | def setStatus(self, status): 62 | self.device.status =status 63 | 64 | def write_file(self): 65 | host = self.device.get('host', as_string=True) 66 | folder = self.device.get('folder', as_string=True) 67 | filename = self.device.get('filename', as_string=True) 68 | fileext = self.device.get('fileext', as_string=True) 69 | format = self.device.get('format', as_string=True) 70 | if format == '': 71 | format = '%s.%s' 72 | 73 | filename = format % (filename, fileext) 74 | filename = os.path.join(host, folder, filename) 75 | 76 | print 'Write to File: ', filename 77 | print 'Mode =', self.device.mode 78 | 79 | # Write to Master Mapping file: 80 | # maybe add PVs to epics py_example.db for 81 | # sampleX, sampleY, sampleName 82 | mapfile = open(MAPFILENAME, 'a') 83 | mapfile.write("%s , %s , %s : %s\n" % (xpos, ypos, samplename, filename)) 84 | mapfile.close() 85 | 86 | # 87 | # put real commands here 88 | if self.device.mode == 0: 89 | self.setMessage(' writing (Mode 0)....') 90 | time.sleep(2.0) 91 | os.system(burt_command0) 92 | # newval = epics.caget(SOME_OTHER_PV) 93 | 94 | # write data from PVLIST 95 | try: 96 | fout = open(filename, 'w') 97 | except: 98 | print 'could not open file %s for writing ' % filename 99 | for pv, desc in self.env_pvs: 100 | try: 101 | fout.write('%s || %s || %s \n' % (pv.pvname,pv.char_value,desc)) 102 | except: 103 | pass 104 | fout.close() 105 | 106 | self.setMessage(' wrote %s ' % filename) 107 | # 108 | 109 | elif self.device.mode == 1: 110 | self.setMessage(' writing (Mode 1)....') 111 | time.sleep(3.0) 112 | self.setMessage(' cleaning up ....') 113 | time.sleep(1.0) 114 | 115 | def run(self): 116 | self.setMessage('Starting...') 117 | self.device.request = 0 # == 'caput Py:EXT:request 0' 118 | self.setTime() 119 | time.sleep(0.1) 120 | while True: 121 | time.sleep(0.1) 122 | self.setTime() 123 | if self.device.request == 1: # start # 'caget Py:EXT:request =? 1' 124 | self.setMessage(' Starting ....') 125 | self.setStatus(1) 126 | if self.device.mode == 0: # Py:EXT:mode 127 | self.write_file() 128 | 129 | elif self.device.mode == 1: # Py:EXT:mode 130 | # self.read_mode0_file() 131 | # self.write_something_else() 132 | self.write_file() 133 | 134 | self.setMessage(' Done.') 135 | self.setStatus(0) 136 | self.device.request = 0 137 | elif self.device.request == 0: # stop 138 | pass 139 | elif self.device.request == 2: # pause 140 | print 'pause not implemented' 141 | elif self.device.request == 3: # resume 142 | print 'resume not implemented' 143 | elif self.device.request == 4: # shutdown 144 | break 145 | 146 | self.setMessage(' Shutting down ....') 147 | self.setStatus(3) 148 | self.setTime(0) 149 | 150 | if __name__ == '__main__': 151 | prefix = 'Py:EXT' 152 | if len(sys.argv) > 1: 153 | prefix = sys.argv[1] 154 | 155 | c = Collector(prefix) 156 | c.run() 157 | -------------------------------------------------------------------------------- /Legacy/ExampleApp/PVLIST_Example.TXT: -------------------------------------------------------------------------------- 1 | S:SRcurrentAI.VAL Storage Ring Current 2 | ID13ds:Energy.VAL Undulator Energy 3 | ID13ds:Gap.VAL Undulator Gap 4 | ID13ds:HarmonicValue.VAL Undulator Harmonic 5 | BL13:SRID:HPosition.VAL ID H Beam Pos 6 | BL13:SRID:HAngle.VAL ID H Beam Angle 7 | BL13:SRID:VPosition.VAL ID V Beam Pos 8 | BL13:SRID:VAngle.VAL ID V Beam Angle 9 | 13IDA:E:Energy.VAL Mono Energy drive val 10 | 13IDA:E:E_RBV Mono Energy readback 11 | 13IDA:m17.RBV FOE Mono angle readback 12 | 13XRM:ION:Current I0 current (uA) 13 | 13XRM:ION:FluxAbs I0 absorbed flux (Hz) 14 | 13XRM:ION:FluxOut I0 transmitted flux (Hz) 15 | 13XRM:ION:Volts I0 input voltage 16 | 13XRM:ION:Amp I0 input amplifier 17 | 13XRM:ION:Length I0 chamber length (cm) 18 | 13XRM:ION:Gas I0 chamber gas 19 | 13IDE:A1sens_unit.VAL I0 sensitivity units 20 | 13IDE:A1sens_num.VAL I0 sensitivity value 21 | 13IDE:A2sens_unit.VAL I1 sensitivity units 22 | 13IDE:A2sens_num.VAL I1 sensitivity value 23 | 13IDE:A3sens_unit.VAL I2 sensitivity units 24 | 13IDE:A3sens_num.VAL I2 sensitivity value 25 | 13IDA:m1.VAL FOE H Slit Pos 26 | 13IDA:m2.VAL FOE H Slit Wid 27 | 13IDA:m3.VAL FOE H Slit Pos 28 | 13IDA:m4.VAL FOE V Slit Wid 29 | 13IDA:m10.VAL FOE Mono Yout 30 | 13IDA:m11.VAL FOE Mono Yin 31 | 13IDA:m12.VAL FOE Mono Yup 32 | 13IDA:m13.VAL FOE Mono Cage Y 33 | 13IDA:m14.VAL FOE Mono Z 34 | 13IDE:m1.VAL Spect Table Yup 35 | 13IDE:m2.VAL Spect Table Yin 36 | 13IDE:m3.VAL Spect Table Yout 37 | 13IDE:m4.VAL Spect Table Xup 38 | 13IDE:m5.VAL Spect Table Xdn 39 | 13IDE:m6.VAL Spect Table Ybase 40 | 13IDE:m13.VAL Table Slit Bottom 41 | 13IDE:m14.VAL Table Slit Top 42 | 13IDE:m15.VAL Table Slit Inboard 43 | 13IDE:m16.VAL Table Slit Outboard 44 | 13IDE:m25.VAL KB V mirror Ypos 45 | 13IDE:m26.VAL KB V mirror Yrot 46 | 13IDE:m27.VAL KB V mirror Fup 47 | 13IDE:m28.VAL KB V mirror Fdn 48 | 13IDE:m29.VAL KB H mirror Xpos 49 | 13IDE:m30.VAL KB H mirror Xrot 50 | 13IDE:m31.VAL KB H mirror Fup 51 | 13IDE:m32.VAL KB H mirror Fdn 52 | 13IDE:scan1.P1PV scan1 Positioner #1 53 | 13IDE:scan2.P1PV scan2 Positioner #1 54 | 13XRM:m1.VAL Sample Stage Fine X 55 | 13XRM:m2.VAL Sample Stage Fine Y 56 | 13XRM:m3.VAL Sample Theta 57 | 13XRM:m4.VAL Sample Stage Coarse X 58 | 13XRM:m5.VAL Sample Stage Coarse Z (focus) 59 | 13XRM:m6.VAL Sample Stage Coarse Y (vert) 60 | 13IDE:m9.VAL Vortex X 61 | 13IDE:m10.VAL Vortex Y 62 | 13IDE:m11.VAL Vortex Z 63 | -------------------------------------------------------------------------------- /Legacy/ExampleApp/example.adl: -------------------------------------------------------------------------------- 1 | 2 | file { 3 | name="/home/newville/Codes/Epics/epicsapps/ExampleApp/example.adl" 4 | version=030104 5 | } 6 | display { 7 | object { 8 | x=123 9 | y=311 10 | width=385 11 | height=300 12 | } 13 | clr=14 14 | bclr=2 15 | cmap="" 16 | gridSpacing=5 17 | gridOn=1 18 | snapToGrid=1 19 | } 20 | "color map" { 21 | ncolors=65 22 | colors { 23 | ffffff, 24 | ececec, 25 | dadada, 26 | c8c8c8, 27 | bbbbbb, 28 | aeaeae, 29 | 9e9e9e, 30 | 919191, 31 | 858585, 32 | 787878, 33 | 696969, 34 | 5a5a5a, 35 | 464646, 36 | 2d2d2d, 37 | 000000, 38 | 00d800, 39 | 1ebb00, 40 | 339900, 41 | 2d7f00, 42 | 216c00, 43 | fd0000, 44 | de1309, 45 | be190b, 46 | a01207, 47 | 820400, 48 | 5893ff, 49 | 597ee1, 50 | 4b6ec7, 51 | 3a5eab, 52 | 27548d, 53 | fbf34a, 54 | f9da3c, 55 | eeb62b, 56 | e19015, 57 | cd6100, 58 | ffb0ff, 59 | d67fe2, 60 | ae4ebc, 61 | 8b1a96, 62 | 610a75, 63 | a4aaff, 64 | 8793e2, 65 | 6a73c1, 66 | 4d52a4, 67 | 343386, 68 | c7bb6d, 69 | b79d5c, 70 | a47e3c, 71 | 7d5627, 72 | 58340f, 73 | 99ffff, 74 | 73dfff, 75 | 4ea5f9, 76 | 2a63e4, 77 | 0a00b8, 78 | ebf1b5, 79 | d4db9d, 80 | bbc187, 81 | a6a462, 82 | 8b8239, 83 | 73ff6b, 84 | 52da3b, 85 | 3cb420, 86 | 289315, 87 | 1a7309, 88 | } 89 | } 90 | text { 91 | object { 92 | x=8 93 | y=8 94 | width=140 95 | height=20 96 | } 97 | "basic attribute" { 98 | clr=54 99 | } 100 | textix="Example App" 101 | } 102 | "text entry" { 103 | object { 104 | x=90 105 | y=105 106 | width=285 107 | height=25 108 | } 109 | control { 110 | chan="$(P)$(Q):host" 111 | clr=14 112 | bclr=0 113 | } 114 | format="string" 115 | limits { 116 | } 117 | } 118 | "text entry" { 119 | object { 120 | x=90 121 | y=135 122 | width=285 123 | height=25 124 | } 125 | control { 126 | chan="$(P)$(Q):folder" 127 | clr=14 128 | bclr=0 129 | } 130 | format="string" 131 | limits { 132 | } 133 | } 134 | "text entry" { 135 | object { 136 | x=90 137 | y=165 138 | width=285 139 | height=25 140 | } 141 | control { 142 | chan="$(P)$(Q):filename" 143 | clr=14 144 | bclr=0 145 | } 146 | format="string" 147 | limits { 148 | } 149 | } 150 | text { 151 | object { 152 | x=10 153 | y=110 154 | width=50 155 | height=18 156 | } 157 | "basic attribute" { 158 | clr=14 159 | } 160 | textix="Host" 161 | } 162 | text { 163 | object { 164 | x=10 165 | y=140 166 | width=50 167 | height=18 168 | } 169 | "basic attribute" { 170 | clr=14 171 | } 172 | textix="Folder" 173 | } 174 | text { 175 | object { 176 | x=10 177 | y=170 178 | width=50 179 | height=18 180 | } 181 | "basic attribute" { 182 | clr=14 183 | } 184 | textix="Name" 185 | } 186 | "message button" { 187 | object { 188 | x=295 189 | y=40 190 | width=75 191 | height=20 192 | } 193 | control { 194 | chan="$(P)$(Q):request" 195 | clr=14 196 | bclr=55 197 | } 198 | label="Start" 199 | press_msg="1" 200 | } 201 | "message button" { 202 | object { 203 | x=295 204 | y=70 205 | width=75 206 | height=20 207 | } 208 | control { 209 | chan="$(P)$(Q):request" 210 | clr=14 211 | bclr=55 212 | } 213 | label="Stop" 214 | press_msg="0" 215 | } 216 | "text entry" { 217 | object { 218 | x=90 219 | y=200 220 | width=150 221 | height=25 222 | } 223 | control { 224 | chan="$(P)$(Q):format" 225 | clr=14 226 | bclr=0 227 | } 228 | format="string" 229 | limits { 230 | } 231 | } 232 | text { 233 | object { 234 | x=10 235 | y=205 236 | width=50 237 | height=18 238 | } 239 | "basic attribute" { 240 | clr=14 241 | } 242 | textix="Format" 243 | } 244 | "text entry" { 245 | object { 246 | x=90 247 | y=230 248 | width=150 249 | height=25 250 | } 251 | control { 252 | chan="$(P)$(Q):fileext" 253 | clr=14 254 | bclr=0 255 | } 256 | format="string" 257 | limits { 258 | } 259 | } 260 | text { 261 | object { 262 | x=10 263 | y=235 264 | width=50 265 | height=18 266 | } 267 | "basic attribute" { 268 | clr=14 269 | } 270 | textix="Extension" 271 | } 272 | menu { 273 | object { 274 | x=90 275 | y=70 276 | width=100 277 | height=22 278 | } 279 | control { 280 | chan="$(P)$(Q):mode" 281 | clr=54 282 | bclr=1 283 | } 284 | } 285 | text { 286 | object { 287 | x=10 288 | y=75 289 | width=60 290 | height=16 291 | } 292 | "basic attribute" { 293 | clr=14 294 | } 295 | textix="Mode" 296 | } 297 | "text update" { 298 | object { 299 | x=10 300 | y=40 301 | width=250 302 | height=18 303 | } 304 | monitor { 305 | chan="$(P)$(Q):status" 306 | clr=24 307 | bclr=1 308 | } 309 | format="string" 310 | limits { 311 | } 312 | } 313 | "text update" { 314 | object { 315 | x=10 316 | y=265 317 | width=370 318 | height=18 319 | } 320 | monitor { 321 | chan="$(P)$(Q):message" 322 | clr=24 323 | bclr=1 324 | } 325 | format="string" 326 | limits { 327 | } 328 | } 329 | "text update" { 330 | object { 331 | x=150 332 | y=10 333 | width=225 334 | height=18 335 | } 336 | monitor { 337 | chan="$(P)$(Q):timestamp" 338 | clr=24 339 | bclr=1 340 | } 341 | format="string" 342 | limits { 343 | } 344 | } 345 | -------------------------------------------------------------------------------- /Legacy/ExampleApp/py_exapp.db: -------------------------------------------------------------------------------- 1 | # This database contains fields used to allow 2 | # communication between XRF_Collector and clients 3 | 4 | record(mbbo,"$(P)$(Q):status") { 5 | field(DESC,"Read State") 6 | field(VAL, "0") 7 | field(ZRVL,"0") 8 | field(ZRST,"Done") 9 | field(ONVL,"1") 10 | field(ONST,"Collecting") 11 | field(TWVL,"2") 12 | field(TWST,"Writing") 13 | field(THVL,"3") 14 | field(THST,"Not Connected") 15 | } 16 | 17 | record(mbbo,"$(P)$(Q):mode") { 18 | field(DESC,"Collection Mode") 19 | field(VAL, "0") 20 | field(ZRVL,"0") 21 | field(ZRST,"Empty") 22 | field(ONVL,"1") 23 | field(ONST,"Background") 24 | field(TWVL,"2") 25 | field(TWST,"Data") 26 | field(THVL,"3") 27 | field(THST,"Disconnected") 28 | } 29 | 30 | record(ao,"$(P)$(Q):counttime") { 31 | field(DESC,"Collection Time") 32 | field(PREC,"2") 33 | field(VAL,"1.0") 34 | } 35 | 36 | record(mbbo,"$(P)$(Q):request") { 37 | field(DESC,"Client Requests Collection") 38 | field(VAL, "0") 39 | field(ZRVL,"0") 40 | field(ZRST,"Stop") 41 | field(ONVL,"1") 42 | field(ONST,"Start") 43 | field(TWVL,"2") 44 | field(TWST,"Pause") 45 | field(THVL,"3") 46 | field(THST,"Resume") 47 | field(FRVL,"4") 48 | field(FRST,"Shutdown") 49 | } 50 | 51 | record(waveform,"$(P)$(Q):host") { 52 | field(DTYP,"Soft Channel") 53 | field(NELM,"128") 54 | field(FTVL,"CHAR") 55 | field(DESC, "file host") 56 | } 57 | 58 | record(waveform,"$(P)$(Q):folder") { 59 | field(DTYP,"Soft Channel") 60 | field(DESC, "file directory") 61 | field(NELM,"128") 62 | field(FTVL,"CHAR") 63 | } 64 | 65 | record(waveform,"$(P)$(Q):filename") { 66 | field(DTYP,"Soft Channel") 67 | field(DESC, "file name") 68 | field(NELM,"128") 69 | field(FTVL,"CHAR") 70 | } 71 | 72 | record(stringin,"$(P)$(Q):fileext") { 73 | field(DTYP,"Soft Channel") 74 | field(DESC, "file extension") 75 | field(VAL, "") 76 | } 77 | 78 | record(stringin,"$(P)$(Q):format") { 79 | field(DTYP,"Soft Channel") 80 | field(DESC, "file format string") 81 | field(VAL, "%s.%s") 82 | } 83 | 84 | 85 | record(waveform,"$(P)$(Q):message") { 86 | field(DTYP,"Soft Channel") 87 | field(DESC,"status message") 88 | field(NELM,"128") 89 | field(FTVL,"CHAR") 90 | } 91 | 92 | 93 | record(stringin,"$(P)$(Q):timestamp") { 94 | field(DTYP,"Soft Channel") 95 | field(DESC, "timestamp as string") 96 | field(VAL, "Starting") 97 | } 98 | 99 | record(longout,"$(P)$(Q):unixtime") { 100 | field(DTYP,"Soft Channel") 101 | field(DESC, "timestamp as integer") 102 | field(VAL, 0) 103 | } 104 | 105 | record(waveform,"$(P)$(Q):arg1") { 106 | field(DTYP,"Soft Channel") 107 | field(DESC,"user arg 1") 108 | field(NELM,"128") 109 | field(FTVL,"CHAR") 110 | } 111 | 112 | record(waveform,"$(P)$(Q):arg2") { 113 | field(DTYP,"Soft Channel") 114 | field(DESC,"user arg 2") 115 | field(NELM,"128") 116 | field(FTVL,"CHAR") 117 | } 118 | 119 | record(waveform,"$(P)$(Q):arg3") { 120 | field(DTYP,"Soft Channel") 121 | field(DESC,"user arg 3") 122 | field(NELM,"128") 123 | field(FTVL,"CHAR") 124 | } 125 | -------------------------------------------------------------------------------- /Legacy/ExampleApp/py_exapp.req: -------------------------------------------------------------------------------- 1 | $(P)$(Q):host 2 | $(P)$(Q):folder 3 | $(P)$(Q):filename 4 | $(P)$(Q):fileext 5 | $(P)$(Q):format 6 | -------------------------------------------------------------------------------- /Legacy/ExampleApp/st_cmd.txt: -------------------------------------------------------------------------------- 1 | # Example Python App: 2 | dbLoadRecords("py_app.db","P=Py:,Q=EXT") 3 | 4 | -------------------------------------------------------------------------------- /Legacy/ExampleApp/start_app: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from ExampleApp import Collector 3 | 4 | from epics import caget, caput 5 | import time 6 | import sys 7 | import getopt 8 | 9 | PREFIX = 'Py:EXT' 10 | 11 | Force_Restart = False 12 | Running_Msg = ''' 13 | Example Collector seems to be running fine. 14 | use "start_app -f" to force a restart 15 | ''' 16 | 17 | 18 | try: 19 | opts, args = getopt.getopt(sys.argv[1:], "f",["force"]) 20 | for k,v in opts: 21 | if k in ("-f", "--force"): Force_Restart = True 22 | except: 23 | pass # opts,args = {},() 24 | 25 | 26 | last_time = caget('%s:unixtime' % PREFIX) 27 | 28 | 29 | if (time.time()-last_time > 10) or Force_Restart: 30 | if Force_Restart: 31 | caput('%s:request' % PREFIX, 4) 32 | print 'Waiting for shutdown .... ' 33 | time.sleep(3.) 34 | 35 | print 'starting Collector...' 36 | c= Collector(PREFIX) 37 | c.run() 38 | else: 39 | print Running_Msg 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /Legacy/IonChamber/check_ionchamber.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2.3 2 | 3 | from IonChamber import * 4 | kill_old_process() 5 | time.sleep(0.1) 6 | daemonize_with_pidfile(main, pidfile=pidfile) 7 | -------------------------------------------------------------------------------- /Legacy/IonChamber/start_ionchamber.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import sys 4 | sys.path.insert(0, '/Users/epics/Codes/epicsapps/IonChamber') 5 | 6 | from ionchamber import start_ionchamber, get_lastupdate, kill_old_process 7 | 8 | import time 9 | 10 | prefix = '13XRM:ION:' 11 | if len(sys.argv) > 1: 12 | prefix = sys.argv[1] 13 | 14 | last_time = get_lastupdate(prefix=prefix) 15 | if abs(time.time() - last_time) > 5.0: 16 | kill_old_process() 17 | time.sleep(1.0) 18 | start_ionchamber(prefix=prefix) 19 | else: 20 | print('IonChamber running OK at ', time.ctime()) 21 | -------------------------------------------------------------------------------- /Legacy/MCADisplay/base/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyepics/epicsapps/90cff8b8f6f6ed5c001f52ff47f06fc0ae1b5e82/Legacy/MCADisplay/base/__init__.py -------------------------------------------------------------------------------- /Legacy/MCADisplay/lib/__init__.py: -------------------------------------------------------------------------------- 1 | from mcadisplay import MCADisplay 2 | -------------------------------------------------------------------------------- /Legacy/MCADisplay/pyepics_mcadisplay.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import wx 4 | import sys 5 | 6 | # if (len(sys.argv) > 1 and sys.argv[1].startswith('-d')): 7 | from lib.mcadisplay import MCADisplay 8 | #else: 9 | # from epicsapps.mcadisplay import MCADisplay 10 | 11 | if __name__ == '__main__': 12 | app = wx.App() 13 | MCADisplay().Show(True) 14 | app.MainLoop() 15 | -------------------------------------------------------------------------------- /Legacy/MCADisplay/setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from distutils.core import setup, setup_keywords 4 | 5 | deps = ('wx', 'epics', 'numpy', 'matplotlib') 6 | 7 | setup(name = 'epicsapp_mcadisplay', 8 | version = '0.2', 9 | author = 'Matthew Newville', 10 | author_email = 'newville@cars.uchicago.edu', 11 | license = 'BSD', 12 | description = 'Epics MCA Display', 13 | package_dir = {'epicsapps.mcadisplay': 'lib', 14 | 'epicsapps': 'base'}, 15 | packages = ['epicsapps', 'epicsapps.mcadisplay'], 16 | data_files = [('bin', ['pyepics_mcadisplay.py'])]) 17 | 18 | 19 | errmsg = 'WARNING: pyepics_mcadisplay requires Python module "%s"' 20 | for mod in deps: 21 | try: 22 | a = __import__(mod) 23 | except ImportError: 24 | print errmsg % mod 25 | -------------------------------------------------------------------------------- /Legacy/MCADisplay/setup_py2app.py: -------------------------------------------------------------------------------- 1 | """ 2 | This is a setup.py script generated by py2applet 3 | 4 | Usage: 5 | python setup.py py2app 6 | """ 7 | 8 | from setuptools import setup 9 | 10 | APP = ['pyepics_mcadisplay.py'] 11 | DATA_FILES = [] 12 | OPTIONS = {'argv_emulation': True, 13 | 'includes': 'epics,wx,numpy,matplotlib'} 14 | 15 | setup( 16 | app=APP, 17 | data_files=DATA_FILES, 18 | options={'py2app': OPTIONS}, 19 | setup_requires=['py2app'], 20 | ) 21 | -------------------------------------------------------------------------------- /Legacy/MotorSetup/base/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyepics/epicsapps/90cff8b8f6f6ed5c001f52ff47f06fc0ae1b5e82/Legacy/MotorSetup/base/__init__.py -------------------------------------------------------------------------------- /Legacy/MotorSetup/lib/__init__.py: -------------------------------------------------------------------------------- 1 | from motor_setup import EpicsMotorSetupApp 2 | -------------------------------------------------------------------------------- /Legacy/MotorSetup/lib/configfile.py: -------------------------------------------------------------------------------- 1 | import os 2 | from ConfigParser import ConfigParser 3 | from cStringIO import StringIO 4 | 5 | def get_appdir(appname): 6 | """gives a user-writeable application directory for config files etc""" 7 | appbase = None 8 | if os.name == 'nt': 9 | for var in ('APPDATA', 'HOMEPATH', 'HOME', 10 | 'USERPROFILE', 'HOMEDRIVE'): 11 | appbase = os.environ.get(var, None) 12 | if appbase is not None: 13 | break 14 | if appbase is None: 15 | appbase = 'C:' 16 | else: 17 | appbase = os.environ.get('HOME', '/tmp') 18 | appname = '.%s' % appname 19 | appbase = os.path.join(appbase, appname) 20 | if not os.path.exists(appbase): 21 | os.makedirs(appbase) 22 | if not os.path.isdir(appbase): 23 | raise RuntimeError('%s is not a directory' % appbas) 24 | return appbase 25 | 26 | default_config=""" 27 | [dbs] 28 | most_recent=Motors.mdb 29 | """ 30 | 31 | class MotorsConfig(object): 32 | basename = 'epics_motorss' 33 | sections = ('dbs',) 34 | 35 | def __init__(self, name=None): 36 | self.conffile = name 37 | if name is None: 38 | self.conffile = os.path.join(get_appdir(self.basename), 39 | 'config.ini') 40 | self.conf = {} 41 | self.cp = ConfigParser() 42 | self.read() 43 | 44 | def read(self): 45 | for s in self.sections: 46 | self.conf[s] = {} 47 | if not os.path.exists(self.conffile): 48 | self.cp.readfp(StringIO(default_config)) 49 | self.cp.read(self.conffile) 50 | 51 | for sect in self.sections: 52 | if self.cp.has_section(sect): 53 | for opt in self.cp.options(sect): 54 | if opt is None: 55 | continue 56 | self.conf[sect][opt] = self.cp.get(sect, opt) 57 | 58 | def write(self, fname=None): 59 | if fname is None: 60 | fname = self.conffile 61 | out = [] 62 | for sect in self.sections: 63 | out.append('[%s]\n' % sect) 64 | if sect == 'dbs': 65 | maxcount = 10 66 | if sect in self.conf: 67 | count = 0 68 | for key in sorted(self.conf[sect]): 69 | val = self.conf[sect][key] 70 | if count < maxcount: 71 | out.append('%s = %s\n' % (key, val)) 72 | count += 1 73 | 74 | fout = open(fname, 'w') 75 | fout.writelines(out) 76 | fout.close() 77 | 78 | def get_dblist(self): 79 | dblist = [self.conf['dbs'].get('most_recent', '')] 80 | for key in sorted(self.conf['dbs'].keys()): 81 | val = self.conf['dbs'][key] 82 | if val is None: continue 83 | val = val.strip() 84 | if (key != 'most_recent' and len(val) > 0): 85 | dblist.append(val) 86 | return dblist 87 | 88 | def set_current_db(self, dbname): 89 | dblist = self.get_dblist() 90 | idx = 1 91 | newlist = [dbname] 92 | for name in dblist: 93 | if len(name.strip()) > 0 and name not in newlist: 94 | key = 'v%2.2i' % idx 95 | self.conf['dbs'][key] = name 96 | idx += 1 97 | 98 | self.conf['dbs']['most_recent'] = dbname 99 | -------------------------------------------------------------------------------- /Legacy/MotorSetup/lib/create_init_motordb.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | provides make_newdb() function to create an empty Epics Instrument Library 4 | """ 5 | import motordb 6 | if __name__ == '__main__': 7 | dbname = 'GSECARS_Motors.mdb' 8 | motordb.make_newdb(dbname) 9 | print '''%s created and initialized.''' % dbname 10 | 11 | -------------------------------------------------------------------------------- /Legacy/MotorSetup/lib/mysql_settings.py: -------------------------------------------------------------------------------- 1 | # host, username, password, db name for mysql use 2 | # please fill in the proper values! 3 | HOST = '' 4 | USER = '' 5 | PASSWD = '' 6 | DBNAME = '' 7 | -------------------------------------------------------------------------------- /Legacy/MotorSetup/lib/new_mysql_db.py: -------------------------------------------------------------------------------- 1 | import motordb 2 | motordb.make_newdb('a', server='mysql') 3 | 4 | -------------------------------------------------------------------------------- /Legacy/MotorSetup/lib/utils.py: -------------------------------------------------------------------------------- 1 | import wx 2 | import wx.lib.filebrowsebutton as filebrowse 3 | import os 4 | import shutil 5 | import time 6 | import epics 7 | 8 | from epics.wx.utils import pack 9 | 10 | FileBrowser = filebrowse.FileBrowseButtonWithHistory 11 | 12 | ALL_EXP = wx.ALL|wx.EXPAND 13 | MDB_WILDCARD = 'Motors DB Files (*.mdb)|*.mdb|All files (*.*)|*.*' 14 | 15 | def get_pvdesc(pvname): 16 | desc = pref = pvname 17 | if '.' in pvname: 18 | pref = pvname[:pvname.find('.')] 19 | t0 = time.time() 20 | descpv = epics.PV(pref + '.DESC') 21 | if descpv.connect(): 22 | desc = descpv.get() 23 | return desc 24 | 25 | 26 | 27 | def dumpsql(dbname, fname=None): 28 | """ dump SQL statements for an sqlite db""" 29 | if fname is None: 30 | fname = '%s_dump.sql' % dbname 31 | os.system('echo .dump | sqlite3 %s > %s' % (dbname, fname)) 32 | 33 | def backup_versions(fname, max=5): 34 | """keep backups of a file -- up to 'max', in order""" 35 | if not os.path.exists(fname): 36 | return 37 | base, ext = os.path.splitext(fname) 38 | for i in range(max-1, 0, -1): 39 | fb0 = "%s_%i%s" % (base, i, ext) 40 | fb1 = "%s_%i%s" % (base, i+1, ext) 41 | if os.path.exists(fb0): 42 | try: 43 | shutil.move(fb0, fb1) 44 | except: 45 | pass 46 | shutil.move(fname, "%s_1%s" % (base, ext)) 47 | 48 | 49 | def save_backup(fname, outfile=None): 50 | """make a copy of fname""" 51 | if not os.path.exists(fname): 52 | return 53 | if outfile is None: 54 | base, ext = os.path.splitext(fname) 55 | outfile = "%s_BAK%s" % (base, ext) 56 | return shutil.copy(fname, outfile) 57 | 58 | def set_font_with_children(widget, font, dsize=None): 59 | cfont = widget.GetFont() 60 | font.SetWeight(cfont.GetWeight()) 61 | if dsize == None: 62 | dsize = font.PointSize - cfont.PointSize 63 | else: 64 | font.PointSize = cfont.PointSize + dsize 65 | widget.SetFont(font) 66 | for child in widget.GetChildren(): 67 | set_font_with_children(child, font, dsize=dsize) 68 | 69 | 70 | class GUIColors(object): 71 | def __init__(self): 72 | self.bg = wx.Colour(240,240,230) 73 | self.nb_active = wx.Colour(254,254,195) 74 | self.nb_area = wx.Colour(250,250,245) 75 | self.nb_text = wx.Colour(10,10,180) 76 | self.nb_activetext = wx.Colour(80,10,10) 77 | self.title = wx.Colour(80,10,10) 78 | self.pvname = wx.Colour(10,10,80) 79 | 80 | class HideShow(wx.Choice): 81 | def __init__(self, parent, default=True, size=(100, -1)): 82 | wx.Choice.__init__(self, parent, -1, size=size) 83 | self.choices = ('Hide', 'Show') 84 | self.Clear() 85 | self.SetItems(self.choices) 86 | self.SetSelection({False:0, True:1}[default]) 87 | 88 | class YesNo(wx.Choice): 89 | def __init__(self, parent, defaultyes=True, size=(75, -1)): 90 | wx.Choice.__init__(self, parent, -1, size=size) 91 | self.choices = ('No', 'Yes') 92 | self.Clear() 93 | self.SetItems(self.choices) 94 | self.SetSelection({False:0, True:1}[defaultyes]) 95 | 96 | def SetChoices(self, choices): 97 | self.Clear() 98 | self.SetItems(choices) 99 | self.choices = choices 100 | 101 | def Select(self, choice): 102 | if isinstance(choice, int): 103 | self.SetSelection(0) 104 | elif choice in self.choices: 105 | self.SetSelection(self.choices.index(choice)) 106 | 107 | class ConnectDialog(wx.Dialog): 108 | """Connect to a recent or existing DB File, or create a new one""" 109 | msg = '''Select Motor DB File, or type a new file name to create a new Motor DB File''' 110 | def __init__(self, parent=None, filelist=None, 111 | title='Select a Motor DB File'): 112 | 113 | wx.Dialog.__init__(self, parent, wx.ID_ANY, title=title) 114 | 115 | panel = wx.Panel(self) 116 | self.colors = GUIColors() 117 | panel.SetBackgroundColour(self.colors.bg) 118 | if parent is not None: 119 | self.SetFont(parent.GetFont()) 120 | 121 | flist = [] 122 | if filelist is not None: 123 | for fname in filelist: 124 | if os.path.exists(fname): 125 | flist.append(fname) 126 | 127 | self.filebrowser = FileBrowser(panel, size=(600, -1)) 128 | self.filebrowser.SetHistory(flist) 129 | self.filebrowser.SetLabel('File:') 130 | self.filebrowser.fileMask = MDB_WILDCARD 131 | 132 | if filelist is not None: 133 | self.filebrowser.SetValue(filelist[0]) 134 | 135 | sizer = wx.BoxSizer(wx.VERTICAL) 136 | sizer.Add(wx.StaticText(panel, label=self.msg), 137 | 0, wx.ALIGN_CENTER|wx.ALL|wx.GROW, 1) 138 | sizer.Add(self.filebrowser, 1, wx.ALIGN_CENTER|wx.ALL|wx.GROW, 1) 139 | sizer.Add(self.CreateButtonSizer(wx.OK| wx.CANCEL), 140 | 1, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 1) 141 | pack(panel, sizer) 142 | sizer = wx.BoxSizer(wx.VERTICAL) 143 | sizer.Add(panel, 0, 0, 0) 144 | pack(self, sizer) 145 | 146 | -------------------------------------------------------------------------------- /Legacy/MotorSetup/pyepics_motorsetup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import wx 4 | import sys 5 | 6 | if (len(sys.argv) > 1 and sys.argv[1].startswith('-d')): 7 | from lib import EpicsMotorSetupApp 8 | else: 9 | from epicsapps.motorsetup import EpicsMotorSetupApp 10 | 11 | if __name__ == '__main__': 12 | server = 'sqlite' 13 | server = 'mysql' 14 | app = EpicsMotorSetupApp(dbname=None, server=server) 15 | app.MainLoop() 16 | 17 | -------------------------------------------------------------------------------- /Legacy/MotorSetup/setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from distutils.core import setup, setup_keywords 4 | 5 | deps = ('wx', 'epics', 'sqlalchemy') 6 | setup(name = 'epicsapp_motorsetup', 7 | version = '0.2', 8 | author = 'Matthew Newville', 9 | author_email = 'newville@cars.uchicago.edu', 10 | license = 'BSD', 11 | description = 'Epics Motor Setup and management', 12 | package_dir = {'epicsapps.motorsetup': 'lib', 'epicsapps': 'base'}, 13 | packages = ['epicsapps', 'epicsapps.motorsetup'], 14 | data_files = [('bin', ['pyepics_motorsetup.py'])]) 15 | 16 | 17 | errmsg = 'WARNING: pyepics_motorsetup requires Python module "%s"' 18 | 19 | for mod in deps: 20 | try: 21 | a = __import__(mod) 22 | except ImportError: 23 | print errmsg % mod 24 | 25 | -------------------------------------------------------------------------------- /Legacy/XRFCollector/XRM_XMAP_PVS.DAT: -------------------------------------------------------------------------------- 1 | S:SRcurrentAI.VAL Storage Ring Current 2 | ID13ds:Energy.VAL Undulator Energy 3 | ID13ds:Gap.VAL Undulator Gap 4 | ID13ds:HarmonicValue.VAL Undulator Harmonic 5 | BL13:SRID:HPosition.VAL ID H Beam Pos 6 | BL13:SRID:HAngle.VAL ID H Beam Angle 7 | BL13:SRID:VPosition.VAL ID V Beam Pos 8 | BL13:SRID:VAngle.VAL ID V Beam Angle 9 | 13IDA:E:Energy.VAL Mono Energy drive val 10 | 13IDA:E:E_RBV Mono Energy readback 11 | 13IDA:m17.RBV FOE Mono angle readback 12 | 13SDD1:dxp1:PeakingTime Vortex dxp1 Peaking Time 13 | 13SDD1:dxp2:PeakingTime Vortex dxp2 Peaking Time 14 | 13SDD1:dxp3:PeakingTime Vortex dxp3 Peaking Time 15 | 13SDD1:dxp4:PeakingTime Vortex dxp4 Peaking Time 16 | 13SDD1:dxp1:InputCountRate Vortex dxp1 Input Count Rate 17 | 13SDD1:dxp2:InputCountRate Vortex dxp2 Input Count Rate 18 | 13SDD1:dxp3:InputCountRate Vortex dxp3 Input Count Rate 19 | 13SDD1:dxp4:InputCountRate Vortex dxp4 Input Count Rate 20 | 13SDD1:dxp1:OutputCountRate Vortex dxp1 Output Count Rate 21 | 13SDD1:dxp2:OutputCountRate Vortex dxp2 Output Count Rate 22 | 13SDD1:dxp3:OutputCountRate Vortex dxp3 Output Count Rate 23 | 13SDD1:dxp4:OutputCountRate Vortex dxp4 Output Count Rate 24 | -------------------------------------------------------------------------------- /Legacy/XRFCollector/iocboot/.svn/all-wcprops: -------------------------------------------------------------------------------- 1 | K 25 2 | svn:wc:ra_dav:version-url 3 | V 52 4 | /svn/epicsapps/!svn/ver/1/trunk/XRFCollector/iocboot 5 | END 6 | XRF_Collect.db 7 | K 25 8 | svn:wc:ra_dav:version-url 9 | V 67 10 | /svn/epicsapps/!svn/ver/1/trunk/XRFCollector/iocboot/XRF_Collect.db 11 | END 12 | XRF_Collect.req 13 | K 25 14 | svn:wc:ra_dav:version-url 15 | V 68 16 | /svn/epicsapps/!svn/ver/1/trunk/XRFCollector/iocboot/XRF_Collect.req 17 | END 18 | st_cmd_text 19 | K 25 20 | svn:wc:ra_dav:version-url 21 | V 64 22 | /svn/epicsapps/!svn/ver/1/trunk/XRFCollector/iocboot/st_cmd_text 23 | END 24 | -------------------------------------------------------------------------------- /Legacy/XRFCollector/iocboot/.svn/entries: -------------------------------------------------------------------------------- 1 | 10 2 | 3 | dir 4 | 22 5 | http://millenia.cars.aps.anl.gov/svn/epicsapps/trunk/XRFCollector/iocboot 6 | http://millenia.cars.aps.anl.gov/svn/epicsapps 7 | 8 | 9 | 10 | 2010-03-02T15:19:54.502916Z 11 | 1 12 | newville 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 2eafd881-6ae8-44cd-9e56-86ae79f26bf0 28 | 29 | XRF_Collect.db 30 | file 31 | 32 | 33 | 34 | 35 | 2010-03-02T15:25:58.082125Z 36 | 0a7fafb4e8dac0f0e72b7ed53072e780 37 | 2010-03-02T15:19:54.502916Z 38 | 1 39 | newville 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 3505 62 | 63 | XRF_Collect.req 64 | file 65 | 66 | 67 | 68 | 69 | 2010-03-02T15:25:58.082125Z 70 | 3489b06650eb38ebc88ce76e18ea08ed 71 | 2010-03-02T15:19:54.502916Z 72 | 1 73 | newville 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 123 96 | 97 | st_cmd_text 98 | file 99 | 100 | 101 | 102 | 103 | 2010-03-02T15:25:58.082125Z 104 | 64ebd38e27fd46ecbfbc2292f2e8f7e2 105 | 2010-03-02T15:19:54.502916Z 106 | 1 107 | newville 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 93 130 | 131 | -------------------------------------------------------------------------------- /Legacy/XRFCollector/iocboot/.svn/text-base/XRF_Collect.db.svn-base: -------------------------------------------------------------------------------- 1 | # This database contains fields used to allow 2 | # communication between XRF_Collector and clients 3 | 4 | grecord(mbbo,"$(P)$(Q):Status") { 5 | field(DESC,"Read State") 6 | field(VAL, "0") 7 | field(ZRVL,"0") 8 | field(ZRST,"Done") 9 | field(ONVL,"1") 10 | field(ONST,"Collecting") 11 | field(TWVL,"2") 12 | field(TWST,"Writing") 13 | field(THVL,"3") 14 | field(THST,"Not Connected") 15 | 16 | } 17 | 18 | grecord(mbbo,"$(P)$(Q):Mode") { 19 | field(DESC,"Auto/Manual Mode") 20 | field(VAL, "0") 21 | field(ZRVL,"0") 22 | field(ZRST,"Manual") 23 | field(ONVL,"1") 24 | field(ONST,"Automatic") 25 | } 26 | 27 | grecord(ao,"$(P)$(Q):CountTime") { 28 | field(DESC,"XRF Collect Time") 29 | field(PREC,"2") 30 | field(VAL,"1.0") 31 | } 32 | 33 | grecord(mbbo,"$(P)$(Q):Request") { 34 | field(DESC,"Client Requests Collection") 35 | field(VAL, "0") 36 | field(ZRVL,"0") 37 | field(ZRST,"Stop") 38 | field(ONVL,"1") 39 | field(ONST,"Start") 40 | field(TWVL,"2") 41 | field(TWST,"Pause") 42 | field(THVL,"3") 43 | field(THST,"Resume") 44 | field(THVL,"4") 45 | field(THST,"Init") 46 | } 47 | 48 | # field(FLNK,"$(P)$(Q):SetRequestBusy") 49 | # grecord(bo,"$(P)$(Q):SetRequestBusy") { 50 | # field(VAL,"1") 51 | # field(OUT,"$(P)$(Q):RequestBusy PP MS") 52 | # } 53 | # 54 | # record(busy,"$(P)$(Q):RequestBusy") { 55 | # } 56 | 57 | 58 | grecord(waveform,"$(P)$(Q):host") { 59 | field(DTYP,"Soft Channel") 60 | field(NELM,"128") 61 | field(FTVL,"CHAR") 62 | field(DESC, "file host") 63 | ## field(VAL, "/cars5/Data") 64 | } 65 | 66 | grecord(waveform,"$(P)$(Q):dir") { 67 | field(DTYP,"Soft Channel") 68 | field(DESC, "file directory") 69 | ## field(VAL, "xas_user ") 70 | field(NELM,"128") 71 | field(FTVL,"CHAR") 72 | } 73 | 74 | grecord(waveform,"$(P)$(Q):subdir") { 75 | field(DTYP,"Soft Channel") 76 | field(DESC, "file subdirectory") 77 | ## field(VAL, "2008 ") 78 | field(NELM,"128") 79 | field(FTVL,"CHAR") 80 | } 81 | 82 | grecord(waveform,"$(P)$(Q):filebase") { 83 | field(DTYP,"Soft Channel") 84 | field(DESC, "file base name") 85 | ## field(VAL, " " ) 86 | field(NELM,"128") 87 | field(FTVL,"CHAR") 88 | } 89 | 90 | grecord(stringin,"$(P)$(Q):format") { 91 | field(DTYP,"Soft Channel") 92 | field(DESC, "file format string") 93 | ## field(VAL, "%s_xrf.%.3i") 94 | } 95 | 96 | 97 | grecord(stringin, "$(P)$(Q):fileext") { 98 | field(DTYP,"Soft Channel") 99 | field(DESC, "file extesion") 100 | field(VAL, "001") 101 | } 102 | 103 | grecord(waveform,"$(P)$(Q):MSG") { 104 | field(DTYP,"Soft Channel") 105 | field(DESC,"status message") 106 | field(NELM,"128") 107 | field(FTVL,"CHAR") 108 | } 109 | 110 | 111 | grecord(stringin,"$(P)$(Q):TSTAMP") { 112 | field(DTYP,"Soft Channel") 113 | field(DESC, "timestamp") 114 | field(VAL, "Starting") 115 | } 116 | 117 | grecord(longout,"$(P)$(Q):UNIXTS") { 118 | field(DTYP,"Soft Channel") 119 | field(DESC, "timestamp") 120 | field(VAL, 0) 121 | } 122 | 123 | grecord(stringin,"$(P)$(Q):det") { 124 | field(DTYP,"Soft Channel") 125 | field(DESC, "detector") 126 | field(VAL, "13SDD1:") 127 | } 128 | 129 | grecord(ao,"$(P)$(Q):nelem") { 130 | field(DESC,"number of detector elements") 131 | field(PREC,"0") 132 | field(VAL,"4") 133 | } 134 | 135 | grecord(waveform,"$(P)$(Q):arg01") { 136 | field(DTYP,"Soft Channel") 137 | field(DESC,"user arg 01") 138 | field(NELM,"128") 139 | field(FTVL,"CHAR") 140 | } 141 | 142 | grecord(waveform,"$(P)$(Q):arg02") { 143 | field(DTYP,"Soft Channel") 144 | field(DESC,"user arg 02") 145 | field(NELM,"128") 146 | field(FTVL,"CHAR") 147 | } 148 | grecord(waveform,"$(P)$(Q):arg03") { 149 | field(DTYP,"Soft Channel") 150 | field(DESC,"user arg 03") 151 | field(NELM,"128") 152 | field(FTVL,"CHAR") 153 | } 154 | -------------------------------------------------------------------------------- /Legacy/XRFCollector/iocboot/.svn/text-base/XRF_Collect.req.svn-base: -------------------------------------------------------------------------------- 1 | $(P)$(Q):host 2 | $(P)$(Q):dir 3 | $(P)$(Q):subdir 4 | $(P)$(Q):filebase 5 | $(P)$(Q):format 6 | $(P)$(Q):fileext 7 | $(P)$(Q):det 8 | $(P)$(Q):nelem 9 | -------------------------------------------------------------------------------- /Legacy/XRFCollector/iocboot/.svn/text-base/st_cmd_text.svn-base: -------------------------------------------------------------------------------- 1 | # XRF Spectra Collector 2 | dbLoadRecords("$(CARS)/CARSApp/Db/XRF_Collect.db","P=13XRM:,Q=XRF") 3 | -------------------------------------------------------------------------------- /Legacy/XRFCollector/iocboot/XRF_Collect.db: -------------------------------------------------------------------------------- 1 | # This database contains fields used to allow 2 | # communication between XRF_Collector and clients 3 | 4 | grecord(mbbo,"$(P)$(Q):Status") { 5 | field(DESC,"Read State") 6 | field(VAL, "0") 7 | field(ZRVL,"0") 8 | field(ZRST,"Done") 9 | field(ONVL,"1") 10 | field(ONST,"Collecting") 11 | field(TWVL,"2") 12 | field(TWST,"Writing") 13 | field(THVL,"3") 14 | field(THST,"Not Connected") 15 | 16 | } 17 | 18 | grecord(mbbo,"$(P)$(Q):Mode") { 19 | field(DESC,"Auto/Manual Mode") 20 | field(VAL, "0") 21 | field(ZRVL,"0") 22 | field(ZRST,"Manual") 23 | field(ONVL,"1") 24 | field(ONST,"Automatic") 25 | } 26 | 27 | grecord(ao,"$(P)$(Q):CountTime") { 28 | field(DESC,"XRF Collect Time") 29 | field(PREC,"2") 30 | field(VAL,"1.0") 31 | } 32 | 33 | grecord(mbbo,"$(P)$(Q):Request") { 34 | field(DESC,"Client Requests Collection") 35 | field(VAL, "0") 36 | field(ZRVL,"0") 37 | field(ZRST,"Stop") 38 | field(ONVL,"1") 39 | field(ONST,"Start") 40 | field(TWVL,"2") 41 | field(TWST,"Pause") 42 | field(THVL,"3") 43 | field(THST,"Resume") 44 | field(THVL,"4") 45 | field(THST,"Init") 46 | } 47 | 48 | # field(FLNK,"$(P)$(Q):SetRequestBusy") 49 | # grecord(bo,"$(P)$(Q):SetRequestBusy") { 50 | # field(VAL,"1") 51 | # field(OUT,"$(P)$(Q):RequestBusy PP MS") 52 | # } 53 | # 54 | # record(busy,"$(P)$(Q):RequestBusy") { 55 | # } 56 | 57 | 58 | grecord(waveform,"$(P)$(Q):host") { 59 | field(DTYP,"Soft Channel") 60 | field(NELM,"128") 61 | field(FTVL,"CHAR") 62 | field(DESC, "file host") 63 | ## field(VAL, "/cars5/Data") 64 | } 65 | 66 | grecord(waveform,"$(P)$(Q):dir") { 67 | field(DTYP,"Soft Channel") 68 | field(DESC, "file directory") 69 | ## field(VAL, "xas_user ") 70 | field(NELM,"128") 71 | field(FTVL,"CHAR") 72 | } 73 | 74 | grecord(waveform,"$(P)$(Q):subdir") { 75 | field(DTYP,"Soft Channel") 76 | field(DESC, "file subdirectory") 77 | ## field(VAL, "2008 ") 78 | field(NELM,"128") 79 | field(FTVL,"CHAR") 80 | } 81 | 82 | grecord(waveform,"$(P)$(Q):filebase") { 83 | field(DTYP,"Soft Channel") 84 | field(DESC, "file base name") 85 | ## field(VAL, " " ) 86 | field(NELM,"128") 87 | field(FTVL,"CHAR") 88 | } 89 | 90 | grecord(stringin,"$(P)$(Q):format") { 91 | field(DTYP,"Soft Channel") 92 | field(DESC, "file format string") 93 | ## field(VAL, "%s_xrf.%.3i") 94 | } 95 | 96 | 97 | grecord(stringin, "$(P)$(Q):fileext") { 98 | field(DTYP,"Soft Channel") 99 | field(DESC, "file extesion") 100 | field(VAL, "001") 101 | } 102 | 103 | grecord(waveform,"$(P)$(Q):MSG") { 104 | field(DTYP,"Soft Channel") 105 | field(DESC,"status message") 106 | field(NELM,"128") 107 | field(FTVL,"CHAR") 108 | } 109 | 110 | 111 | grecord(stringin,"$(P)$(Q):TSTAMP") { 112 | field(DTYP,"Soft Channel") 113 | field(DESC, "timestamp") 114 | field(VAL, "Starting") 115 | } 116 | 117 | grecord(longout,"$(P)$(Q):UNIXTS") { 118 | field(DTYP,"Soft Channel") 119 | field(DESC, "timestamp") 120 | field(VAL, 0) 121 | } 122 | 123 | grecord(stringin,"$(P)$(Q):det") { 124 | field(DTYP,"Soft Channel") 125 | field(DESC, "detector") 126 | field(VAL, "13SDD1:") 127 | } 128 | 129 | grecord(ao,"$(P)$(Q):nelem") { 130 | field(DESC,"number of detector elements") 131 | field(PREC,"0") 132 | field(VAL,"4") 133 | } 134 | 135 | grecord(waveform,"$(P)$(Q):arg01") { 136 | field(DTYP,"Soft Channel") 137 | field(DESC,"user arg 01") 138 | field(NELM,"128") 139 | field(FTVL,"CHAR") 140 | } 141 | 142 | grecord(waveform,"$(P)$(Q):arg02") { 143 | field(DTYP,"Soft Channel") 144 | field(DESC,"user arg 02") 145 | field(NELM,"128") 146 | field(FTVL,"CHAR") 147 | } 148 | grecord(waveform,"$(P)$(Q):arg03") { 149 | field(DTYP,"Soft Channel") 150 | field(DESC,"user arg 03") 151 | field(NELM,"128") 152 | field(FTVL,"CHAR") 153 | } 154 | -------------------------------------------------------------------------------- /Legacy/XRFCollector/iocboot/XRF_Collect.req: -------------------------------------------------------------------------------- 1 | $(P)$(Q):host 2 | $(P)$(Q):dir 3 | $(P)$(Q):subdir 4 | $(P)$(Q):filebase 5 | $(P)$(Q):format 6 | $(P)$(Q):fileext 7 | $(P)$(Q):det 8 | $(P)$(Q):nelem 9 | -------------------------------------------------------------------------------- /Legacy/XRFCollector/iocboot/st.cmd: -------------------------------------------------------------------------------- 1 | # XRF Spectra Collector 2 | dbLoadRecords("$(CARS)/CARSApp/Db/XRF_Collect.db","P=13XRM:,Q=XRF") 3 | -------------------------------------------------------------------------------- /Legacy/XRFCollector/start_xrfcollect: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from MED_Collect import MED_Collector 3 | from epics import PV 4 | import time 5 | import sys 6 | import getopt 7 | 8 | Force_Restart = False 9 | 10 | try: 11 | opts, args = getopt.getopt(sys.argv[1:], "f",["force"]) 12 | for k,v in opts: 13 | if k in ("-f", "--force"): Force_Restart = True 14 | except: 15 | pass # opts,args = {},() 16 | 17 | 18 | ts_pv = PV('13XRM:XRF:UNIXTS') 19 | last_time = ts_pv.get() 20 | 21 | Running_Msg = '''XRF collection seems to be running fine. 22 | use "start_xrfcollect -f" to force a restart 23 | ''' 24 | 25 | if (time.time()-last_time > 10) or Force_Restart: 26 | print 'starting XRF Collect...' 27 | med= MED_Collector() 28 | med.run() 29 | else: 30 | print Running_Msg 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md LICENSE MANIFEST.in setup.py requirements.txt 2 | exclude *.pyc core.* *~ *.pdf 3 | recursive-exclude epicsapps/__pycache__ * 4 | recursive-exclude epicsapps *.pyc 5 | 6 | recursive-include epicsapps *.py 7 | recursive-include epicsapps/icons * 8 | recursive-include examples * 9 | recursive-include doc 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Epics Applications 2 | 3 | A collection of applications for Epics using Python 4 | 5 | ## install 6 | 7 | On Linux, you will need to make sure that wxPython is installed. For an Anaconda envirornment, 8 | 9 | conda -c conda-forge install wxpython 10 | 11 | will work. Then, 12 | 13 | pip install epicsapps 14 | 15 | will install everything else you need. 16 | 17 | 18 | ## StripChart 19 | 20 | A StripChart Application, showing live time series of Epics PVs 21 | 22 | ## areaDetector Viewer 23 | 24 | A viewer for areaDetector Viewers, with good image properties including 25 | automatic contrast levels, a user-configurable Zoom-box, and simple 26 | configuration file to add which control variables are shown. 27 | 28 | ## Epics Instruments 29 | 30 | A GUI application to group any PVs together into named Instruments, and then 31 | save and restore positions for these Instruments by name. That is, you can 32 | group for 4 motors together, calling them "Slits", and then save and restore 33 | positions called "1x1 mm" and "2x2 mm", etc. 34 | 35 | ## Epics PV Logger 36 | 37 | Two related appplications to save and view time-series data for selected 38 | PVs. Data collection can be run from a command line application, reading a 39 | YAML file to configure which PVs are saved. Data is saved into plain text 40 | files in a single folder. A GUI application can help create and modify the 41 | configuration file, and start collection. The GUI application can also 42 | browse and display the data collected into the PVlog folder. 43 | 44 | ## Microscope Viewer 45 | 46 | A GUI application for viewing an controlling a sample stage with a microscope 47 | camera. This combines aspects of both areaDetector Viewer and Epics Instruments, 48 | as positions of the sample stege can be saved and restored by name. 49 | 50 | ## IonChamber calculations 51 | 52 | A commandline application to connect to and read Ion Chamber settings 53 | (voltages, amplifier gains) and X-ray energy to compute the fluxes absorbed 54 | and transmitted by an Ion Chamber. 55 | -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | This repository contains Epics Channel Access Applications using PyEpics. 2 | The intention herer is to include programs that are either general-purpose 3 | stand-alone applications or specific-use applications that can be viewed as 4 | examples. 5 | 6 | Each application should live in its own folder, and have its own 7 | installation scripts and documentation. The applicaions may vary in their 8 | completeness and level of documentation. In addition to pyepics, many of 9 | these applications will require additional third party libraries, such as 10 | wxPython, numpy, and matplotlib. These should be explicitly listed in the 11 | Dependencies below. 12 | 13 | Some applications may expect to interact with specific Epics databases or 14 | define new (usually small. trivial) databases and possibly Epics Display 15 | files (.adls for MEDM, for example). These should be included in the 16 | directory. 17 | 18 | Complete documentation can be found at http://pyepics.github.com/epicsapps/ 19 | 20 | 21 | A brief description of the current Epics Applications: 22 | 23 | ====================================== 24 | Folder: Instruments 25 | App Name: Epics Instruments 26 | Dependencies: wxPython, sqlalchemy. 27 | 28 | Epics Instruments is a GUI app that allows a user to group Epics PVs into a 29 | named "Instrument". Many Instruments can be defined, with each Instrument 30 | having its own Tab in a Notebook window. PVs will be displayed as 31 | name/value pairs, in a "type-aware" manner for easier interaction and data 32 | entry. Each Instrument has a set of named positions that save the values 33 | for all PVs in that instrument when the position is named. Any of the 34 | named positions can be "restored", that is, all the PVs moved to the values 35 | when the position was named at any time. 36 | 37 | The set of defined instruments shown in the application, and all the 38 | named positions are stored in a single file -- an sqlite3 database 39 | file. Multiple instances of the program can be running on the same 40 | subnet and even computer without stepping on one another, though 41 | the application works hard to prevent a second instance from using 42 | an open-and-working definition file. 43 | 44 | ====================================== 45 | Folder: IonChamber 46 | App Name: Ion Chamber 47 | Dependencies: numpy 48 | 49 | Ion Chamber reads ion chamber currents and amplifier settings, uses these 50 | to calculate the absorbed, incident, and transmitted x-ray flux, and writes 51 | these out to Epics PVs. It probably makes several implicit assumptions 52 | about units and equipment setup specific to APS/GSECARS, but may provide 53 | useful example of writing a state-program with pyepics. 54 | 55 | ====================================== 56 | Folder: SampleStage 57 | App Name: GSECARS XYZ Sample Stage 58 | Dependencies: wx 59 | 60 | A GUI app for controlling a sample stage, including saving/restoring 61 | positions and capturing images (web-cam only, currently) associated 62 | with each position. 63 | 64 | ====================================== 65 | 66 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | INSTALLDIR = /www/apache/htdocs/software/python/pyepicsapps 11 | 12 | 13 | # Internal variables. 14 | PAPEROPT_a4 = -D latex_paper_size=a4 15 | PAPEROPT_letter = -D latex_paper_size=letter 16 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 17 | 18 | .PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest latexpdf 19 | .PHONY: all install 20 | 21 | html: 22 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 23 | @echo 24 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 25 | 26 | epicsapps.pdf: latex 27 | cd $(BUILDDIR)/latex && make all-pdf 28 | cp -pr $(BUILDDIR)/latex/epicsapps.pdf ./ 29 | 30 | all: html epicsapps.pdf 31 | 32 | install: all 33 | cp -pr $(BUILDDIR)/latex/epicsapps.pdf $(INSTALLDIR)/. 34 | cp -pr $(BUILDDIR)/html/* $(INSTALLDIR)/. 35 | 36 | help: 37 | @echo "Please use \`make ' where is one of" 38 | @echo " html to make standalone HTML files" 39 | @echo " dirhtml to make HTML files named index.html in directories" 40 | @echo " pickle to make pickle files" 41 | @echo " json to make JSON files" 42 | @echo " htmlhelp to make HTML files and a HTML help project" 43 | @echo " qthelp to make HTML files and a qthelp project" 44 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 45 | @echo " changes to make an overview of all changed/added/deprecated items" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | -rm -rf $(BUILDDIR)/* 51 | 52 | dirhtml: 53 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 56 | 57 | pickle: 58 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 59 | @echo 60 | @echo "Build finished; now you can process the pickle files." 61 | 62 | json: 63 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 64 | @echo 65 | @echo "Build finished; now you can process the JSON files." 66 | 67 | htmlhelp: 68 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 69 | @echo 70 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 71 | ".hhp project file in $(BUILDDIR)/htmlhelp." 72 | 73 | latex: 74 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) _build/latex 75 | @echo 76 | @echo "Build finished; the LaTeX files are in _build/latex." 77 | @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ 78 | "run these through (pdf)latex." 79 | 80 | latexpdf: 81 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) _build/latex 82 | @echo "Running LaTeX files through pdflatex..." 83 | make -C _build/latex all-pdf 84 | @echo "pdflatex finished; the PDF files are in _build/latex." 85 | 86 | changes: 87 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 88 | @echo 89 | @echo "The overview file is in $(BUILDDIR)/changes." 90 | 91 | linkcheck: 92 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 93 | @echo 94 | @echo "Link check complete; look for any errors in the above output " \ 95 | "or in $(BUILDDIR)/linkcheck/output.txt." 96 | 97 | doctest: 98 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 99 | @echo "Testing of doctests in the sources finished, look at the " \ 100 | "results in $(BUILDDIR)/doctest/output.txt." 101 | -------------------------------------------------------------------------------- /doc/_templates/indexsidebar.html: -------------------------------------------------------------------------------- 1 |

Contents

2 | 3 | 11 |

12 | 13 |

EpicsApps

14 | Current version: {{ release }}
15 | Install:   pip install epicsapps
16 | Develop:   github.com 17 | -------------------------------------------------------------------------------- /doc/_templates/layout.html: -------------------------------------------------------------------------------- 1 | {% extends "!layout.html" %} 2 | {%- block relbar1 %} 3 |
5 |
6 | {{ relbar() }}{% endblock %} 7 | -------------------------------------------------------------------------------- /doc/ad_display.rst: -------------------------------------------------------------------------------- 1 | .. _ad_viewer: 2 | 3 | 4 | Area Detector Display 5 | ==================================== 6 | 7 | Epics Area Detector Display is a wxPython GUI application for viewing 8 | images from an Epics Area Detector. To run this application, simply 9 | run AD_Display.py at the command line:: 10 | 11 | epicsapps adviewer 12 | 13 | This will start with a file browser to search for an AreaDetector 14 | configuration file, which uses yaml syntax. 15 | 16 | A few examples of configuration files are given at 17 | https://github.com/pyepics/epicsapps/tree/master/examples/areaDetector, with a sample lookng like this:: 18 | 19 | name: IDA Beam Viewer 20 | prefix: '13IDAPG1:' 21 | title: AD Display / IDA Beam Viewer 22 | camera_attributes: [Acquire, ArrayCounter, ArrayCounter_RBV, NumImages, NumImages_RBV, 23 | AcquireTime, AcquireTime_RBV, TriggerMode, TriggerMode_RBV] 24 | colormaps: [gray, magma, inferno, plasma, viridis, coolwarm, hot, jet] 25 | colormode: Mono 26 | default_rotation: 0 27 | enabled_plugins: [image1, Over1, Over2, Over3, Over4, ROI1, ROI2, JPEG1, TIFF1] 28 | epics_controls: 29 | - [Trigger Mode, 'cam1:TriggerMode', true, pvenum, _RBV, 150, 10] 30 | - [Image Mode, 'cam1:ImageMode', true, pvenum, _RBV, 150, 10] 31 | - ['# Images', 'cam1:NumImages', true, pvfloat, _RBV, 100, 10] 32 | - [Acquire Time, 'cam1:AcquireTime', true, pvfloat, _RBV, 100, 10] 33 | - [Acquire Period, 'cam1:AcquirePeriod', true, pvfloat, _RBV, 100, 10] 34 | - [TIFF File Path, 'TIFF1:FilePath', true, pvtctrl, false, 250, 10] 35 | - [Acquire Status, 'cam1:Acquire', true, pvtext, false, 250, 10] 36 | filesaver: 'TIFF1:' 37 | free_run_time: 0.2 38 | image_attributes: [ArrayData, UniqueId_RBV] 39 | show_thumbnail: true 40 | thumbnail_size: 100 41 | use_filesaver: true 42 | workdir: /home/user 43 | scandb_instrument: Pinhole Tank BPM 44 | 45 | 46 | This describes how the `adviewer` application will connect to the areaDetector, 47 | including which PVs to include for a very basic widget controls with a limited 48 | set of Process Variables described such as those for starting and stopping the 49 | acquisition. This configuration file will generate an interface like this: 50 | 51 | 52 | .. image:: images/AD_Display.png 53 | 54 | 55 | 56 | 57 | Note that the `epics_controls` is a list of data for PVs to be displayed in the 58 | upper left portion of the window. From the configuration file above, 59 | note:: 60 | 61 | epics_controls: 62 | - [Trigger Mode, 'cam1:TriggerMode', true, pvenum, _RBV, 150, 10] 63 | - [Image Mode, 'cam1:ImageMode', true, pvenum, _RBV, 150, 10] 64 | 65 | 66 | Each item in the `epics_controls` list has values of 67 | 68 | * display name, here "Trigger Mode" 69 | * Epics PV to use, here "cam1:TriggerMode" 70 | * whether to prepend the AD PV Prefix, here "13IDAPG1:" to the PV name, here "true". 71 | * what kind of PV it is -- enum, float, text, which will determine what type of widget is use, heree "pvenum". 72 | * what suffix (if any) to use for a "readback PV", here "_RBV", common for many AD PVs 73 | * the size of the widget in pixels, here 150 74 | * the font size for the widget, here 10. 75 | 76 | 77 | The areaDetector display will allow changing color table lookup using 78 | a few supplied colortables (from matplotlib) - reversing that color 79 | table is allowded. 80 | 81 | 82 | Contrast levels can be set using a percentage value to clip the 83 | intensity range. That is, a value of 1 will set the intensity range to 84 | be from the 1%% to 99%% intensity level of the entire image. This can 85 | be a very useful option for many areaDetectors either with bad pixels 86 | or high dynamic range, as the full scale image is often unusable. 87 | 88 | In addition, the display shows a "thumbnail image" with an ajustable 89 | size. At startup, this is centered on the image, but clicking the 90 | mouse on the image will center that portion of the image in the Zoom 91 | box. 92 | 93 | Finally, if an Epics ScanDB data is setup with `Instruments` and a 94 | postgresql database, saved positions from one or more instruments can 95 | be included in the display, for example to move a camera or shutter 96 | into saved positions. 97 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Epics Applications doc 4 | 5 | import sys, os 6 | from packaging.version import parse as version_parse 7 | import epicsapps 8 | 9 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.mathjax', 10 | 'sphinx.ext.napoleon', 'sphinxcontrib.video', 11 | 'sphinx_copybutton', 'numpydoc'] 12 | 13 | # Add any paths that contain templates here, relative to this directory. 14 | templates_path = ['_templates'] 15 | 16 | project = "Epics Applications" 17 | copyright = "2025, Matthew Newville, The University of Chicago" 18 | 19 | html_title = "Epics Applications Using PyEpics" 20 | html_short_title = "EpicsApps" 21 | 22 | release = version_parse(epicsapps.__version__).base_version 23 | 24 | 25 | source_suffix = {'.rst': 'restructuredtext'} 26 | exclude_trees = ['_build'] 27 | default_role = None 28 | source_encoding = 'utf-8' 29 | 30 | add_function_parentheses = True 31 | 32 | add_module_names = True 33 | pygments_style = 'sphinx' 34 | master_doc = 'index' 35 | 36 | html_theme_path = ['sphinx_theme'] 37 | html_theme = 'bizstyle' 38 | 39 | html_static_path = ['_static'] 40 | html_sidebars = { 41 | 'index': ["indexsidebar.html", "sourcelink.html", "searchbox.html"], 42 | "**": [ "localtoc.html", "relations.html", "sourcelink.html", "searchbox.html"] 43 | } 44 | 45 | html_domain_indices = False 46 | html_use_index = True 47 | html_show_sourcelink = True 48 | -------------------------------------------------------------------------------- /doc/images/AD_Display.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyepics/epicsapps/90cff8b8f6f6ed5c001f52ff47f06fc0ae1b5e82/doc/images/AD_Display.png -------------------------------------------------------------------------------- /doc/images/InstMain_DXP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyepics/epicsapps/90cff8b8f6f6ed5c001f52ff47f06fc0ae1b5e82/doc/images/InstMain_DXP.png -------------------------------------------------------------------------------- /doc/images/InstMain_Mixed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyepics/epicsapps/90cff8b8f6f6ed5c001f52ff47f06fc0ae1b5e82/doc/images/InstMain_Mixed.png -------------------------------------------------------------------------------- /doc/images/InstMain_Stage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyepics/epicsapps/90cff8b8f6f6ed5c001f52ff47f06fc0ae1b5e82/doc/images/InstMain_Stage.png -------------------------------------------------------------------------------- /doc/images/Inst_Edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyepics/epicsapps/90cff8b8f6f6ed5c001f52ff47f06fc0ae1b5e82/doc/images/Inst_Edit.png -------------------------------------------------------------------------------- /doc/images/Inst_GoTo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyepics/epicsapps/90cff8b8f6f6ed5c001f52ff47f06fc0ae1b5e82/doc/images/Inst_GoTo.png -------------------------------------------------------------------------------- /doc/images/Inst_MEDM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyepics/epicsapps/90cff8b8f6f6ed5c001f52ff47f06fc0ae1b5e82/doc/images/Inst_MEDM.png -------------------------------------------------------------------------------- /doc/images/Inst_Settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyepics/epicsapps/90cff8b8f6f6ed5c001f52ff47f06fc0ae1b5e82/doc/images/Inst_Settings.png -------------------------------------------------------------------------------- /doc/images/Inst_Startup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyepics/epicsapps/90cff8b8f6f6ed5c001f52ff47f06fc0ae1b5e82/doc/images/Inst_Startup.png -------------------------------------------------------------------------------- /doc/images/MotorSetup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyepics/epicsapps/90cff8b8f6f6ed5c001f52ff47f06fc0ae1b5e82/doc/images/MotorSetup.png -------------------------------------------------------------------------------- /doc/images/pvlogger_eventval.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyepics/epicsapps/90cff8b8f6f6ed5c001f52ff47f06fc0ae1b5e82/doc/images/pvlogger_eventval.png -------------------------------------------------------------------------------- /doc/images/pvlogger_mainview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyepics/epicsapps/90cff8b8f6f6ed5c001f52ff47f06fc0ae1b5e82/doc/images/pvlogger_mainview.png -------------------------------------------------------------------------------- /doc/images/pvlogger_plotenum.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyepics/epicsapps/90cff8b8f6f6ed5c001f52ff47f06fc0ae1b5e82/doc/images/pvlogger_plotenum.png -------------------------------------------------------------------------------- /doc/images/pvlogger_plotone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyepics/epicsapps/90cff8b8f6f6ed5c001f52ff47f06fc0ae1b5e82/doc/images/pvlogger_plotone.png -------------------------------------------------------------------------------- /doc/images/pvlogger_plotsel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyepics/epicsapps/90cff8b8f6f6ed5c001f52ff47f06fc0ae1b5e82/doc/images/pvlogger_plotsel.png -------------------------------------------------------------------------------- /doc/images/pvlogger_table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyepics/epicsapps/90cff8b8f6f6ed5c001f52ff47f06fc0ae1b5e82/doc/images/pvlogger_table.png -------------------------------------------------------------------------------- /doc/images/stripchart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyepics/epicsapps/90cff8b8f6f6ed5c001f52ff47f06fc0ae1b5e82/doc/images/stripchart.png -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | .. PyEpics Applications 2 | 3 | .. _pyepics: https://pyepics.github.io/pyepics 4 | .. _EpicsApps: https://pyepics.github.io/epicsapps 5 | .. _PyEpics: https://pyepics.github.io/pyepics 6 | .. _wxPython: https://www.wxpython.org/ 7 | .. _matplotlib: https://matplotlib.org/ 8 | 9 | 10 | 11 | Epics Applications with PyEpics 12 | ===================================== 13 | 14 | EpicsApps is a collection of Epics Applications written in python, 15 | using the `pyepics`_ library for interacting with Epics devices 16 | through Channel Access. Many of these are GUI Applications, built 17 | with `wxPython`_, and some use other Python libraries for interacting 18 | wih data. While a part of the goal for this project is to demonstrate 19 | how one can build complex programs with `pyepics`_, the applications 20 | here are intended to be useful to beamline scientists and end-users at 21 | facilities such as synchrotrons that use the Epics control system. 22 | 23 | The main applications included in EpicsApps include 24 | 25 | * :ref:`stripchart`: A GUI application to show a "live plot" of the recent 26 | history of a set of PV values. Time ranges and ranges for Y 27 | values can be changed, and data can be saved to plain text files. 28 | 29 | * :ref:`ad_viewer`: A GUI application to control and view images 30 | from an Epics Area Detector. Controls for stqrting acquistion, 31 | changing image mode, exposure time, and frame rate can also be 32 | included using a simple configuration file. The end-user can 33 | change color-table and contrast level, as well as use a Zoom Box 34 | to enhance portions of the image. 35 | 36 | * :ref:`instruments`: A GUI application to organize PVs, by grouping 37 | them into user-named "Instruments". Each Instrument can have a 38 | set of named positions that the end-user can save and restore. 39 | 40 | * :ref:`pvlogger`: A command-line and GUI application to collect and log 41 | time-series of a handful of PV values into plain text files in a 42 | folder in a manner that can be easily reviewed. 43 | 44 | * *Sample Microscope*: A GSECARS-specific GUI for moving a set of 45 | motors for a sample stage, grabbing microscope images from a 46 | webcam, and saving named positions. 47 | 48 | 49 | 50 | .. toctree:: 51 | :maxdepth: 2 52 | 53 | installation 54 | stripchart 55 | ad_display 56 | instruments 57 | pvlogger 58 | other 59 | -------------------------------------------------------------------------------- /doc/installation.rst: -------------------------------------------------------------------------------- 1 | Installation and Getting Started 2 | ==================================== 3 | 4 | 5 | The latest version of the `epicsapps` package is |release|, which can be installed with:: 6 | 7 | pip install epicsapps 8 | 9 | 10 | Many of the Epics Applications provide GUI forms and displays using wxPython. 11 | 12 | For Windows and macOS X, all dependencies can be reliably installed 13 | with the `pip` command above, including Python using Anaconda Python. 14 | 15 | On Linux, PyPI does not have a binary package for wxPython for Linux, 16 | so `pip install` may try to build wxPython from source. This requires 17 | a large number of development packages on Linux, and rarely works 18 | without some effort. On the other hand, there are conda packages for 19 | wxPython from the `conda-forge` channel. In addition, some 20 | system-provided Python packaging also include wxPython with its 21 | packaging tools.q 22 | 23 | Using Anaconda Python is recommended and common for many scientific 24 | applications, and is common at many facilities using Epics. 25 | If using Anaconda Python, you can first do:: 26 | 27 | conda install -c conda-forge wxpython 28 | 29 | 30 | and then:: 31 | 32 | pip install epicsapps 33 | 34 | 35 | This approach will work on all systems, and is recommended on Linux. 36 | 37 | 38 | Getting Started 39 | ------------------------ 40 | 41 | Installing the epicsapps package will install a command-line script `epicsapps` 42 | that can be used to launch the main epicsapps GUI applications. This works 43 | as:: 44 | 45 | epicsapps [options] appname [filename] 46 | 47 | 48 | where `options` can be 49 | 50 | * `-h`, `--help`: show this help message and exit 51 | * `-m`, `--makeicons` create desktop and start menu icons 52 | * `-p`, `--prompt` prompt for configuration on startup 53 | * `-n`, `--no-prompt` suppress prompt, use default configuration 54 | * `-c`, `--cli` run as a command-line program. 55 | 56 | and `appname` can be one of 57 | 58 | * `adviewer` [filename] Area Detector Viewer 59 | * `instruments` [filename] Epics Instruments 60 | * `microscope` [filename] Sample Microscope Viewer 61 | * `pvlogger` [filename] PV Logger data collection 62 | * `pvlogview` PV Logger data Viewer 63 | * `stripchart` PV Stripchart 64 | 65 | 66 | and `filename` is an optional configuration YAML file. 67 | 68 | 69 | .. _install_icons: 70 | 71 | Creating Desktop Shortcuts 72 | ----------------------------- 73 | 74 | Running:: 75 | 76 | epicsapps -m 77 | 78 | will create a folder called "Epics Apps" on your desktop with links to launch 79 | the main GUI applications. 80 | -------------------------------------------------------------------------------- /doc/instruments.rst: -------------------------------------------------------------------------------- 1 | .. _instruments: 2 | 3 | Epics Instruments 4 | ==================================== 5 | 6 | Epics Instruments is a GUI application (using wxPython) that lets any user: 7 | 8 | * Organize PVs into Instruments: a named collection of PVs 9 | * Manage Instruments with modern Notebook-tab interface. 10 | * Save Positions for any Instrument by name. 11 | * Restore Positions for any Instrument by name. 12 | * Remember Settings for all definitions into a single file that can be loaded later. 13 | * Multiple Users can be using multiple instrument files at any one time. 14 | 15 | It was originally written to replace and organize the multitude of similar MEDM 16 | screens that appear at many workstations using Epics. 17 | 18 | 19 | Running Epics Instruments 20 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ 21 | 22 | To run Epics Instruments, use:: 23 | 24 | epicsapps instruments 25 | 26 | 27 | or click on the icon. 28 | 29 | A small window to select an Epics Instrument File, like this 30 | 31 | .. image:: images/Inst_Startup.png 32 | :width: 50% 33 | 34 | If this is your first time using the application, choose a name, and hit return 35 | to start a new Instrument File. The next time you run Epics Instruments, it 36 | should remember which files you have recently used, and present you with a 37 | drop-down list of Instrument Files. Since all the definitions, positions, and 38 | settings are saved in a single file, restoring this file will recall the 39 | earlier session of instrument definitions and saved positions. 40 | 41 | 42 | An Epics **Instrument** is a collection of PVs. Each Instrument will also 43 | have a collection of **Positions**, which are just the locations of all the 44 | PVs in the instrument at the time the Position was saved. Like a PV, each 45 | Instrument and each Position for an Instrument has a unique name. 46 | 47 | 48 | Defining a New Instrument 49 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 50 | 51 | To define a new Instrument, select **Create New Instrument** from the 52 | Instruments Menu. A screen will appear in which you can name the 53 | instrument and the PVs that belong to the Instrument. 54 | 55 | If you add a few PVs and click OK, the PVs will connect, and you will see a 56 | screen something like this 57 | 58 | .. image:: images/InstMain_Stage.png 59 | :width: 75% 60 | 61 | 62 | Editing an Exisiting Instrument 63 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 64 | 65 | .. image:: images/Inst_Edit.png 66 | :width: 40% 67 | 68 | 69 | The Instrument File 70 | ~~~~~~~~~~~~~~~~~~~~~~~ 71 | 72 | All the information for definitions of your Instruments and their Positions 73 | are saved in a single file -- the Instruments file, with a default 74 | extension of '.ein' (Epics INstruments). You can use many different 75 | Instrument Files for different domains of use. 76 | 77 | The Instrument File is an SQLite database file, and can be browsed and 78 | manipulated with external tools. Of course, this can be a very efficient 79 | way of corrupting the data, so do this with caution. A further note of 80 | caution is to avoid having a single Instrument file open by multiple 81 | applications -- this can also cause corruption. The Instrument files can 82 | be moved around and copied without problems. 83 | -------------------------------------------------------------------------------- /doc/other.rst: -------------------------------------------------------------------------------- 1 | Ion Chamber 2 | ========================== 3 | 4 | This non-GUI application is synchrotron-beamline specific. It reads 5 | several settings for the photo-current of an ion chamber and calculates 6 | absorbed and transmitted flux in photons/sec, and writes these back to PVs 7 | (an associated .db file and medm .adl file are provided). The script runs 8 | in a loop, updating the flux values continuously. 9 | ==================================== 10 | Ion Chamber 11 | ==================================== 12 | 13 | This application, like the XRF Collector, provides a simple Epics interface 14 | control of a non-trivial device. Again, the main point of showing it 15 | here is as an example of using a python process to interact with a custom 16 | Epics database to provide a customized way to acquire data. While the 17 | standard Epics solution would be to write a state-notation-language program 18 | that runs in an IOC, the approach here minimizes Epics coding to writing a 19 | simple database, and putting all the logic and control into a python script 20 | that runs as a long-running process along-side an IOC process. 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /doc/publish_docs.sh: -------------------------------------------------------------------------------- 1 | installdir='/www/apache/htdocs/software/python/pyepicsapps' 2 | docbuild='doc/_build' 3 | appname='epicsapps' 4 | dockit=$appname_docs.tgz 5 | 6 | cd doc 7 | echo '# Making docs' 8 | make all 9 | cd ../ 10 | 11 | echo '# Building tarball of docs' 12 | mkdir _tmpdoc 13 | cp -pr doc/$appname.pdf _tmpdoc/. 14 | cp -pr doc/_build/html/* _tmpdoc/. 15 | cd _tmpdoc 16 | tar czf ../../$dockit . 17 | cd .. 18 | rm -rf _tmpdoc 19 | 20 | # 21 | 22 | echo "# Switching to gh-pages branch" 23 | git checkout gh-pages 24 | 25 | if [ $? -ne 0 ] ; then 26 | echo ' failed.' 27 | exit 28 | fi 29 | 30 | echo "# Make sure this script is updated!" 31 | git checkout master publish_docs.sh 32 | if [ $? -ne 0 ] ; then 33 | echo ' failed.' 34 | exit 35 | fi 36 | 37 | tar xzf ../$dockit . 38 | 39 | echo "# commit changes to gh-pages branch" 40 | echo '## git commit -am "changed docs" ' 41 | 42 | if [ $? -ne 0 ] ; then 43 | echo ' failed.' 44 | exit 45 | fi 46 | 47 | echo "# Pushing docs to github" 48 | echo '## git push ' 49 | 50 | 51 | echo "# switch back to master branch" 52 | git checkout master 53 | 54 | if [ $? -ne 0 ] ; then 55 | echo ' failed.' 56 | exit 57 | fi 58 | 59 | # install locally 60 | echo "# Installing docs to CARS web pages" 61 | echo '## cp ../$dockit $installdir/../. ' 62 | 63 | cd $installdir 64 | if [ $? -ne 0 ] ; then 65 | echo ' failed.' 66 | exit 67 | fi 68 | 69 | tar xvf ../$dockit . 70 | -------------------------------------------------------------------------------- /doc/stripchart.rst: -------------------------------------------------------------------------------- 1 | .. _stripchart: 2 | 3 | 4 | Strip Chart Display 5 | ==================================== 6 | 7 | StripChart is a GUI application for viewing the time series of PVs as 8 | a strip chart. It feature interactive graphics, with click-and-drag 9 | zooming, updating the plotted time range, saving figures as 10 | high-quality PNGs, and saving data to ASCII files. Stripchart is 11 | inspired somewhat by the classic Epics Stripchart application written 12 | with X/Motif, but has many differences. 13 | 14 | 15 | Running Stripchart 16 | ~~~~~~~~~~~~~~~~~~~~~~ 17 | 18 | To run the Stripchart application from the command line, use:: 19 | 20 | epicsapps stripchart 21 | 22 | 23 | A sample display would look like this: 24 | 25 | .. image:: images/stripchart.png 26 | 27 | 28 | Usage 29 | ~~~~~~~~~ 30 | 31 | To use, Add PVs to be monitored in the upper left entry. Once that PV 32 | connects, it will be added to the drop-down menus for each of the 4 33 | available traces. Colors, Y-ranges, and descriptions (used for the 34 | Y-axis labels) can be altered. To save these settings for a PV, press 35 | "Save PV Settings" in the upper right. 36 | 37 | 38 | The time range (in time from the present) can be adjusted on the 39 | right-hand side, just above the plot. 40 | 41 | The "Pause" and "Resume" buttons pause and then resumes the plotting 42 | of new values so that you can zoom in on parts of the plot using 43 | Left-Down and Drag. 44 | 45 | Note that data collection of new values will still happen while the 46 | plot is paused, and resuming the plotting will show the most recent 47 | data. 48 | 49 | From the "File" Menu, you can save plain text files with the time 50 | series data for all monitored PVs, or save a PNG image of the 51 | plot. With the mouse over the plot window, Control-C will also copy 52 | the PNG image to the clipboard. 53 | 54 | You can also configure the plot from the "Options" menu. 55 | -------------------------------------------------------------------------------- /epicsapps/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from .apps import (run_epicsapps, run_adviewer, run_instruments, 3 | run_stripchart, run_pvlogger) 4 | from .version import __version__, __version_tuple__ 5 | 6 | -------------------------------------------------------------------------------- /epicsapps/apps.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import numpy 4 | import time 5 | 6 | from argparse import ArgumentParser, RawDescriptionHelpFormatter 7 | 8 | from pyshortcuts import make_shortcut, platform 9 | from pyshortcuts.utils import get_homedir 10 | 11 | from .utils import get_configfolder, HAS_WXPYTHON 12 | 13 | def use_mpl_wxagg(): 14 | """import matplotlib, set backend to WXAgg""" 15 | if HAS_WXPYTHON: 16 | try: 17 | import matplotlib 18 | matplotlib.use('WXAgg', force=True) 19 | return True 20 | except ImportError: 21 | pass 22 | return False 23 | 24 | here, _ = os.path.split(__file__) 25 | icondir = os.path.join(here, 'icons') 26 | 27 | 28 | class EpicsApp: 29 | """ 30 | wrapper for Epics Application 31 | """ 32 | def __init__(self, name, script, icon='epics', folder='Epics Apps', terminal=False): 33 | self.name = name 34 | self.script = "epicsapps {}".format(script) 35 | self.folder = folder 36 | icon_ext = 'ico' 37 | if platform == 'darwin': 38 | icon_ext = 'icns' 39 | self.icon = "%s.%s" % (icon, icon_ext) 40 | self.terminal = terminal 41 | bindir = 'bin' 42 | if platform == 'win': 43 | bindir = 'Scripts' 44 | 45 | self.bindir = os.path.join(sys.prefix, bindir) 46 | 47 | def create_shortcut(self): 48 | script = os.path.join(self.bindir, self.script) 49 | make_shortcut(script, name=self.name, 50 | icon=os.path.join(icondir, self.icon), 51 | terminal=self.terminal, 52 | folder=self.folder) 53 | 54 | APPS = (EpicsApp('Instruments', 'instruments', icon='instrument'), 55 | EpicsApp('Sample Microscope', 'microscope', icon='microscope'), 56 | EpicsApp('areaDetector Viewer', 'adviewer', icon='areadetector'), 57 | EpicsApp('StripChart', 'stripchart', icon='stripchart'), 58 | EpicsApp('PVLogger', 'pvlogviewer', icon='logging'), 59 | ) 60 | 61 | # EpicsApp('Ion Chamber', 'epicsapp ionchamber', icon='ionchamber')) 62 | 63 | def run_instruments(configfile=None, prompt=True): 64 | """Epics Instruments""" 65 | from .instruments import EpicsInstrumentApp 66 | EpicsInstrumentApp(configfile=configfile, prompt=prompt).MainLoop() 67 | 68 | def run_samplemicroscope(configfile=None, prompt=True): 69 | """Sample Microscope""" 70 | from .microscope import MicroscopeApp 71 | MicroscopeApp(configfile=configfile, prompt=prompt).MainLoop() 72 | 73 | def run_adviewer(configfile=None, prompt=True): 74 | """AD Viewer""" 75 | from .areadetector import areaDetectorApp 76 | areaDetectorApp(configfile=configfile, prompt=prompt).MainLoop() 77 | 78 | def run_stripchart(configfile=None, prompt=False): 79 | """StripChart""" 80 | from .stripchart import StripChartApp 81 | StripChartApp(configfile=configfile, prompt=prompt).MainLoop() 82 | 83 | def run_pvlogger(configfile=None, prompt=False, **kws): 84 | """PV Logger Command Line App""" 85 | from .pvlogger import PVLogger 86 | if configfile is not None: 87 | PVLogger(configfile=configfile, prompt=prompt).run() 88 | else: 89 | run_pvlogviewer(prompt=prompt) 90 | 91 | def run_pvlogviewer(prompt=False): 92 | """PV Logger""" 93 | from .pvlogger import PVLoggerApp 94 | PVLoggerApp(prompt=prompt).MainLoop() 95 | 96 | 97 | ## main wrapper program 98 | def run_epicsapps(): 99 | """ 100 | run PyEpics Applications 101 | """ 102 | desc = 'run pyepics applications' 103 | epilog ='''applications: 104 | adviewer [filename] Area Detector Viewer 105 | instruments [filename] Epics Instruments GUI 106 | microscope [filename] Sample Microscope Viewer 107 | pvlogviewer Epics PV Logger Viewer GUI 108 | stripchart Epics PV Stripchart GUI 109 | pvlogger [filenmae] Epics PV Logger data collection CLI 110 | 111 | notes: 112 | applications with the optional filename will look for a yaml-formatted 113 | configuration file in the folder 114 | {:s} 115 | or will prompt for configuration file if one is not found. 116 | '''.format(get_configfolder()) 117 | parser = ArgumentParser(description=desc, 118 | epilog=epilog, 119 | formatter_class=RawDescriptionHelpFormatter) 120 | parser.add_argument('-m', '--makeicons', dest='makeicons', 121 | action='store_true', default=False, 122 | help='create desktop and start menu icons') 123 | parser.add_argument('-p', '--prompt', dest='prompt', 124 | action='store_true', default=None, 125 | help='prompt for configuration on startup') 126 | parser.add_argument('-n', '--no-prompt', dest='no_prompt', 127 | action='store_true', default=False, 128 | help='suppress prompt, use default configuration') 129 | parser.add_argument('-c', '--cli', dest='use_cli', 130 | action='store_true', default=False, 131 | help='use Command-line interface, no GUI (pvlogger only)') 132 | parser.add_argument('appname', nargs='?', help='application name') 133 | parser.add_argument('filename', nargs='?', help='configuration file name') 134 | 135 | args = parser.parse_args() 136 | runner = None 137 | needs_help = False 138 | kwargs = {'configfile': args.filename} 139 | if args.appname is None and args.makeicons is False: 140 | needs_help = True 141 | elif args.makeicons: 142 | for app in APPS: 143 | app.create_shortcut() 144 | else: 145 | if args.filename is None and args.prompt is None: 146 | args.prompt = not args.no_prompt 147 | use_mpl_wxagg() 148 | isapp = args.appname.lower().startswith 149 | kwargs['prompt'] = args.prompt 150 | if isapp('inst'): 151 | runner = run_instruments 152 | elif isapp('micro'): 153 | runner = run_samplemicroscope 154 | elif isapp('strip'): 155 | runner = run_stripchart 156 | elif isapp('pvlogv'): 157 | runner= run_pvlogviewer 158 | kwargs = {} 159 | elif isapp('pvlog'): 160 | runner= run_pvlogger 161 | kwargs['use_cli'] = args.use_cli 162 | elif isapp('ad'): 163 | runner = run_adviewer 164 | else: 165 | needs_help = True 166 | if needs_help: 167 | parser.print_usage() 168 | elif runner is not None: 169 | runner(**kwargs) 170 | -------------------------------------------------------------------------------- /epicsapps/areadetector/__init__.py: -------------------------------------------------------------------------------- 1 | from .ad_display import ADFrame, areaDetectorApp 2 | -------------------------------------------------------------------------------- /epicsapps/areadetector/ad_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from ..utils import ConfigFile, load_yaml, get_default_configfile 4 | 5 | # default camera configuration 6 | _configtext = """ 7 | prefix: None 8 | name: areaDetector 9 | title: 'Epics areaDetector Display' 10 | workdir: '' 11 | filesaver: 'TIFF1:' 12 | use_filesaver: true 13 | default_rotation: 0 14 | show_free_run: false 15 | free_run_time: 0.5 16 | show_1dintegration: false 17 | iconfile: None 18 | colormode: Mono 19 | int1d_trimx: 0 20 | int1d_trimy: 0 21 | int1d_flipx: false 22 | int1d_flipy: true 23 | show_thumbnail: true 24 | thumbnail_size: 100 25 | 26 | enabled_plugins: [image1, Over1, Over2, Over3, Over4, ROI1, ROI2, JPEG1, TIFF1] 27 | image_attributes: [ArrayData, UniqueId_RBV] 28 | 29 | camera_attributes: 30 | - Acquire 31 | - DetectorState_RBV 32 | - ArrayCounter 33 | - ArrayCounter_RBV 34 | - NumImages 35 | - NumImages_RBV 36 | - AcquireTime 37 | - AcquireTime_RBV 38 | - TriggerMode 39 | - TriggerMode_RBV 40 | 41 | 42 | colormaps: [gray, magma, inferno, plasma, viridis, coolwarm, hot] 43 | scandb_instrument: None 44 | 45 | epics_controls: 46 | - ['Trigger Mode', 'cam1:TriggerMode', true, pvenum, _RBV, 150, 10] 47 | - ['Num Images', 'cam1:NumImages', true, pvfloat, _RBV, 100, 10] 48 | - ['Acquire Period', 'cam1:AcquirePeriod', true,pvfloat, _RBV, 100, 10] 49 | - ['Acquire Time', 'cam1:AcquireTime', true, pvfloat, _RBV, 100, 10] 50 | - ['Acquire Status', 'cam1:Acquire', true, pvtext, false, 250, 10] 51 | - ['Acquire Busy', 'cam1:AcquireBusy', true, pvtext, false, 250, 10] 52 | - ['Acquire Message', 'cam1:StatusMessage_RBV', true, pvtext, false, 250, 10] 53 | 54 | """ 55 | 56 | CONFFILE = 'areadetector.yaml' 57 | 58 | class ADConfig(ConfigFile): 59 | def __init__(self, fname='areadetector.yaml', default_config=None): 60 | if default_config is None: 61 | default_config = load_yaml(_configtext) 62 | 63 | ConfigFile.__init__(self, fname, default_config=default_config) 64 | -------------------------------------------------------------------------------- /epicsapps/areadetector/ad_scandbcallback.py: -------------------------------------------------------------------------------- 1 | try: 2 | from epicsscan import ScanDB 3 | except: 4 | ScanDB = None 5 | 6 | def setup_calibration(self, ponifile): 7 | """set up calibration from PONI file""" 8 | calib = read_poni(ponifile) 9 | # if self.image.rot90 in (1, 3): 10 | # calib['rot3'] = np.pi/2.0 11 | self.calib = calib 12 | if HAS_PYFAI: 13 | self.integrator = AzimuthalIntegrator(**calib) 14 | self.show1d_btn.Enable() 15 | else: 16 | self.write('Warning: PyFAI is not installed') 17 | 18 | if self.scandb is not None: 19 | _, calname = os.path.split(ponifile) 20 | self.scandb.set_detectorconfig(calname, json.dumps(calib)) 21 | self.scandb.set_info('xrd_calibration', calname) 22 | -------------------------------------------------------------------------------- /epicsapps/areadetector/calibration_dialog.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import json 4 | import numpy as np 5 | 6 | import wx 7 | from wxutils import (GridPanel, Button, FloatCtrl, SimpleText, LCEN) 8 | 9 | from .xrd_integrator import read_poni 10 | 11 | class CalibrationDialog(wx.Dialog): 12 | """dialog for calibrating energy""" 13 | conv = {'wavelength': (1e10, 'Ang'), 14 | 'pixel1': (1e6, 'microns'), 15 | 'pixel2': (1e6, 'microns'), 16 | 'poni1': (1e3, 'mm'), 17 | 'poni2': (1e3, 'mm'), 18 | 'dist': (1e3, 'mm'), 19 | 'rot1': (180/np.pi, 'deg'), 20 | 'rot2': (180/np.pi, 'deg'), 21 | 'rot3': (180/np.pi, 'deg')} 22 | 23 | def __init__(self, parent, calfile, **kws): 24 | 25 | self.parent = parent 26 | self.calfile = calfile 27 | poni = read_poni(calfile) 28 | 29 | wx.Dialog.__init__(self, parent, wx.ID_ANY, size=(600, 525), 30 | title="Read Calibration File") 31 | 32 | 33 | panel = GridPanel(self, ncols=3, nrows=4, pad=2, itemstyle=LCEN) 34 | 35 | self.wids = wids = {} 36 | 37 | wids['filename'] = SimpleText(panel, calfile) 38 | 39 | _p, fname = os.path.split(calfile) 40 | wids['calname'] = wx.TextCtrl(panel, value=fname, size=(350, -1)) 41 | 42 | wids['ok'] = Button(panel, 'OK', size=(150, -1), action=self.on_apply) 43 | 44 | def add_text(text, dcol=1, newrow=True): 45 | panel.Add(SimpleText(panel, text), dcol=dcol, newrow=newrow) 46 | 47 | add_text(' Calibration file: ', newrow=False) 48 | panel.Add(wids['filename'], dcol=3) 49 | add_text(' Save as : ') 50 | panel.Add(wids['calname'], dcol=3) 51 | 52 | opts = dict(size=(90, -1), digits=5) 53 | for wname in ('wavelength', 'dist', 'pixel1', 'pixel2', 54 | 'poni1', 'poni2', 'rot1', 'rot2', 'rot3'): 55 | scale, units = self.conv[wname] 56 | val = scale*float(poni[wname]) 57 | if wname == 'wavelength': 58 | energy = 12398.4193/val 59 | units = '%s, Energy=%.2f' % (units, energy) 60 | 61 | wids[wname] = FloatCtrl(panel, value=val, size=(100, -1), precision=4) 62 | wids[wname+'_units'] = SimpleText(panel, units) 63 | add_text(' %s:' % wname.title() ) 64 | panel.Add(wids[wname]) 65 | panel.Add(wids[wname+'_units']) 66 | 67 | panel.Add((5, 5)) 68 | panel.Add(wids['ok'], dcol=2, newrow=True) 69 | panel.pack() 70 | 71 | def onDone(self, event=None): 72 | self.Destroy() 73 | 74 | def on_apply(self, event=None): 75 | calname = self.wids['calname'].GetValue() 76 | 77 | calib = {} 78 | for wname in ('wavelength', 'dist', 'pixel1', 'pixel2', 79 | 'poni1', 'poni2', 'rot1', 'rot2', 'rot3'): 80 | scale, units = self.conv[wname] 81 | calib[wname] = self.wids[wname].GetValue()/scale 82 | 83 | if self.scandb is not None: 84 | self.scandb.set_detectorconfig(calname, json.dumps(calib)) 85 | self.scandb.set_info('xrd_calibration', calname) 86 | self.parent.setup_calibration(calib) 87 | self.Destroy() 88 | -------------------------------------------------------------------------------- /epicsapps/areadetector/contrast_control.py: -------------------------------------------------------------------------------- 1 | import wx 2 | 3 | class ContrastControl: 4 | """auto-contrast widgets""" 5 | def __init__(self, parent, default=1, callback=None): 6 | self.levels = ['None'] 7 | for scale in (0.001, 0.01, 0.1, 1.0): 8 | for step in (1, 2, 5): 9 | self.levels.append(str(scale*step)) 10 | 11 | self.callback = callback 12 | self.label = wx.StaticText(parent, label='Contrast Level (%):', 13 | size=(150, -1)) 14 | self.choice = wx.Choice(parent, choices=self.levels, size=(100, -1)) 15 | self.choice.Bind(wx.EVT_CHOICE, self.onChoice) 16 | self.choice.SetSelection(0) 17 | 18 | def set_level_str(self, choice=None): 19 | if choice not in self.levels: 20 | choice = self.levels[0] 21 | self.set_level_int(level=self.levels.index(choice)) 22 | 23 | def set_level_int(self, level=0): 24 | if level < 0 or level > len(self.levels)-1: 25 | level = 0 26 | self.choice.SetSelection(level) 27 | if callable(self.callback): 28 | self.run_callback(level) 29 | 30 | def onChoice(self, event=None): 31 | if callable(self.callback): 32 | self.run_callback(event.GetSelection()) 33 | 34 | def run_callback(self, level): 35 | clevel = self.levels[level] 36 | if clevel == 'None': 37 | flevel = 0 38 | else: 39 | flevel = float(clevel) 40 | self.callback(contrast_level=flevel) 41 | 42 | def advance(self): 43 | self.set_level(self.choice.GetSelection() + 1) 44 | 45 | def retreat(self): 46 | self.set_level(self.choice.GetSelection() - 1) 47 | -------------------------------------------------------------------------------- /epicsapps/areadetector/debugtime.py: -------------------------------------------------------------------------------- 1 | import time 2 | import sys 3 | class debugtime(object): 4 | def __init__(self): 5 | self.clear() 6 | self.add('debugtime init') 7 | 8 | def clear(self): 9 | self.times = [] 10 | 11 | def add(self,msg='', verbose=False): 12 | # print msg 13 | self.times.append((msg,time.time())) 14 | if verbose: 15 | sys.stdout.write("%s\n"% msg) 16 | 17 | def show(self, writer=None, clear=True): 18 | if writer is None: 19 | writer = sys.stdout.write 20 | writer('%s\n' % self.get_report()) 21 | if clear: 22 | self.clear() 23 | 24 | 25 | def get_report(self): 26 | m0, t0 = self.times[0] 27 | tlast= t0 28 | out = ["# %s %s " % (m0,time.ctime(t0))] 29 | lmsg = 0 30 | for m, t in self.times[1:]: 31 | lmsg = max(lmsg, len(m)) 32 | m = '# Message' 33 | m = m + ' '*(lmsg-len(m)) 34 | 35 | out.append("#--------------------" + '-'*lmsg) 36 | out.append('%s Delta(s) Total(s)' % (m)) 37 | for m,t in self.times[1:]: 38 | tt = t-t0 39 | dt = t-tlast 40 | if len(m)0: 42 | msg = "'%s' is not a valid PONI file: missing '%s'" 43 | raise ValueError(msg % (fname, ', '.join(missing))) 44 | return conf 45 | 46 | 47 | class XRD_Integrator(): 48 | def __init__(self, ponifile=None, calibration_callback=None): 49 | self.calib = None 50 | self.azint = None 51 | self.calibration_callback = calibration_callback 52 | self.read_ponifile(ponifile) 53 | 54 | def read_ponifile(self, ponifile): 55 | self.ponifile = ponifile 56 | if self.ponifile is not None: 57 | self.calib = read_poni(self.ponifile) 58 | if HAS_PYFAI: 59 | self.azint = AzimuthalIntegrator(**self.calib) 60 | if callable(self.calibration_callback): 61 | self.calibration_callback(calib=self.calib) 62 | 63 | @property 64 | def enabled(self): 65 | return self.azint is not None 66 | 67 | def integrate1d(self, image, npts=2048, polarization_factor=0.999): 68 | if self.enabled: 69 | opts = dict(polarization_factor=polarization_factor, 70 | unit='q_A^-1', correctSolidAngle=True) 71 | return self.azint.integrate1d(image, npts, **opts) 72 | -------------------------------------------------------------------------------- /epicsapps/icons/areadetector.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyepics/epicsapps/90cff8b8f6f6ed5c001f52ff47f06fc0ae1b5e82/epicsapps/icons/areadetector.icns -------------------------------------------------------------------------------- /epicsapps/icons/areadetector.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyepics/epicsapps/90cff8b8f6f6ed5c001f52ff47f06fc0ae1b5e82/epicsapps/icons/areadetector.ico -------------------------------------------------------------------------------- /epicsapps/icons/camera.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyepics/epicsapps/90cff8b8f6f6ed5c001f52ff47f06fc0ae1b5e82/epicsapps/icons/camera.icns -------------------------------------------------------------------------------- /epicsapps/icons/camera.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyepics/epicsapps/90cff8b8f6f6ed5c001f52ff47f06fc0ae1b5e82/epicsapps/icons/camera.ico -------------------------------------------------------------------------------- /epicsapps/icons/instrument.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyepics/epicsapps/90cff8b8f6f6ed5c001f52ff47f06fc0ae1b5e82/epicsapps/icons/instrument.icns -------------------------------------------------------------------------------- /epicsapps/icons/instrument.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyepics/epicsapps/90cff8b8f6f6ed5c001f52ff47f06fc0ae1b5e82/epicsapps/icons/instrument.ico -------------------------------------------------------------------------------- /epicsapps/icons/logging.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyepics/epicsapps/90cff8b8f6f6ed5c001f52ff47f06fc0ae1b5e82/epicsapps/icons/logging.icns -------------------------------------------------------------------------------- /epicsapps/icons/logging.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyepics/epicsapps/90cff8b8f6f6ed5c001f52ff47f06fc0ae1b5e82/epicsapps/icons/logging.ico -------------------------------------------------------------------------------- /epicsapps/icons/microscope.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyepics/epicsapps/90cff8b8f6f6ed5c001f52ff47f06fc0ae1b5e82/epicsapps/icons/microscope.icns -------------------------------------------------------------------------------- /epicsapps/icons/microscope.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyepics/epicsapps/90cff8b8f6f6ed5c001f52ff47f06fc0ae1b5e82/epicsapps/icons/microscope.ico -------------------------------------------------------------------------------- /epicsapps/icons/motorapp.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyepics/epicsapps/90cff8b8f6f6ed5c001f52ff47f06fc0ae1b5e82/epicsapps/icons/motorapp.ico -------------------------------------------------------------------------------- /epicsapps/icons/stripchart.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyepics/epicsapps/90cff8b8f6f6ed5c001f52ff47f06fc0ae1b5e82/epicsapps/icons/stripchart.icns -------------------------------------------------------------------------------- /epicsapps/icons/stripchart.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyepics/epicsapps/90cff8b8f6f6ed5c001f52ff47f06fc0ae1b5e82/epicsapps/icons/stripchart.ico -------------------------------------------------------------------------------- /epicsapps/instruments/__init__.py: -------------------------------------------------------------------------------- 1 | from .instrument import InstrumentDB 2 | from .InstrumentApp import EpicsInstrumentApp 3 | from .creator import make_newdb 4 | -------------------------------------------------------------------------------- /epicsapps/instruments/configfile.py: -------------------------------------------------------------------------------- 1 | import os 2 | from ..utils import ConfigFile, get_default_configfile, get_configfolder 3 | CONFFILE = 'instruments.yaml' 4 | 5 | class InstrumentConfig(ConfigFile): 6 | def __init__(self, fname='instruments.yaml', default_config=None): 7 | if default_config is None: 8 | default_config = dict(server='sqlite', dbname=None, host=None, 9 | port='5432', user=None, password=None, 10 | recent_dbs=[]) 11 | if fname is None: 12 | fname = os.path.join(get_configfolder(), CONFFILE) 13 | 14 | ConfigFile.__init__(self, fname, default_config=default_config) 15 | -------------------------------------------------------------------------------- /epicsapps/instruments/creator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | provides make_newdb() function to create an empty Epics Instrument Library 4 | 5 | """ 6 | import sys 7 | import os 8 | 9 | from datetime import datetime 10 | 11 | from sqlalchemy.orm import sessionmaker, create_session 12 | from sqlalchemy import (MetaData, create_engine, Table, Column, 13 | Integer, Float, String, Text, DateTime, 14 | ForeignKey, UniqueConstraint) 15 | 16 | from .utils import dumpsql, backup_versions 17 | from .simpledb import SimpleDB 18 | 19 | def PointerCol(name, other=None, keyid='id', **kws): 20 | if other is None: 21 | other = name 22 | return Column("%s_%s" % (name, keyid), None, 23 | ForeignKey('%s.%s' % (other, keyid), **kws)) 24 | 25 | def StrCol(name, size=None, **kws): 26 | if size is None: 27 | return Column(name, Text, **kws) 28 | else: 29 | return Column(name, String(size), **kws) 30 | 31 | def NamedTable(tablename, metadata, keyid='id', nameid='name', 32 | name=True, name_unique=True, 33 | notes=True, attributes=True, cols=None): 34 | args = [Column(keyid, Integer, primary_key=True)] 35 | if name: 36 | args.append(StrCol(nameid, nullable=False, unique=name_unique)) 37 | if notes: 38 | args.append(StrCol('notes')) 39 | if attributes: 40 | args.append(StrCol('attributes')) 41 | if cols is not None: 42 | args.extend(cols) 43 | return Table(tablename, metadata, *args) 44 | 45 | class InitialData: 46 | info = [["version", "1.4"], 47 | ["verify_erase", "1"], 48 | ["verify_move", "1"], 49 | ["verify_overwrite", "1"], 50 | ["epics_prefix", ""], 51 | ["create_date", ''], 52 | ["modify_date", '']] 53 | 54 | pvtype = [['numeric', 'Numeric Value'], 55 | ['enum', 'Enumeration Value'], 56 | ['string', 'String Value'], 57 | ['motor', 'Motor Value']] 58 | 59 | def make_newdb(dbname, server= 'sqlite'): 60 | engine = create_engine('%s:///%s' % (server, dbname)) 61 | metadata = MetaData() 62 | metadata.reflect(engine) 63 | 64 | instrument = NamedTable('instrument', metadata, 65 | cols=[Column('show', Integer, default=1), 66 | Column('display_order', Integer, default=0), 67 | Column('modify_time', DateTime)]) 68 | 69 | command = NamedTable('command', metadata, 70 | cols=[StrCol('command'), 71 | StrCol('arguments'), 72 | StrCol('output_value'), 73 | StrCol('output_name')]) 74 | 75 | 76 | position = Table('position', metadata, 77 | Column('id', Integer, primary_key=True), 78 | StrCol('name', nullable=False), 79 | StrCol('notes'), 80 | StrCol('attributes'), 81 | Column('date', DateTime), 82 | StrCol('image'), 83 | Column('modify_time', DateTime), 84 | Column("instrument_id", ForeignKey('instrument.id')), 85 | UniqueConstraint('name', 'instrument_id')) 86 | 87 | instrument_precommand = NamedTable('instrument_precommand', metadata, 88 | cols=[Column('order', Integer), 89 | PointerCol('command'), 90 | PointerCol('instrument')]) 91 | 92 | instrument_postcommand = NamedTable('instrument_postcommand', metadata, 93 | cols=[Column('order', Integer), 94 | PointerCol('command'), 95 | PointerCol('instrument')]) 96 | 97 | pvtype = NamedTable('pvtype', metadata) 98 | pv = NamedTable('pv', metadata, cols=[PointerCol('pvtype')]) 99 | 100 | instrument_pv = Table('instrument_pv', metadata, 101 | Column('id', Integer, primary_key=True), 102 | PointerCol('instrument'), 103 | PointerCol('pv'), 104 | Column('display_order', Integer, default=0), 105 | Column('order', Integer, default=1)) 106 | 107 | position_pv = Table('position_pv', metadata, 108 | Column('id', Integer, primary_key=True), 109 | StrCol('notes'), 110 | PointerCol('position'), 111 | PointerCol('pv'), 112 | StrCol('value')) 113 | 114 | info = Table('info', metadata, 115 | Column('key', Text, primary_key=True, unique=True), 116 | StrCol('value')) 117 | 118 | metadata.create_all(bind=engine) 119 | 120 | db = SimpleDB(dbname, server=server) 121 | for name, notes in InitialData.pvtype: 122 | db.insert('pvtype', name=name, notes=notes) 123 | 124 | now = datetime.isoformat(datetime.now(), sep=' ') 125 | 126 | for key, value in InitialData.info: 127 | if value == '': 128 | value = now 129 | rows = db.get_rows('info', key=key) 130 | if len(rows) == 0: 131 | rows = db.insert('info', key=key, value=value) 132 | else: 133 | rows = db.update('info', where={'key': key}, value=value) 134 | db.close() 135 | 136 | if __name__ == '__main__': 137 | dbname = 'Test.ein' 138 | backup_versions(dbname) 139 | make_newdb(dbname) 140 | print('''%s created and initialized.''' % dbname) 141 | # dumpsql(dbname) 142 | -------------------------------------------------------------------------------- /epicsapps/instruments/epics_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import time 3 | import epics 4 | import epics.devices 5 | from .instrument import isInstrumentDB, InstrumentDB 6 | 7 | class EpicsInstrumentServer(epics.Device): 8 | """ 9 | Epics Device that attaches to an Instrument Database, as defined 10 | by instrument.db 11 | 12 | """ 13 | _nonpvs = ('_prefix', '_pvs', '_delim', '_request', 14 | '_moving', '_instname', '_inst') 15 | 16 | attrs = ('TSTAMP', 'UNIXTS', 'ExecCommand', 'Move', 17 | 'InstName', 'PosName', 'InstOK', 'PosOK', 18 | 'CommandName', 'aCommandOK', 19 | 'CommandArg1', 'CommandArg2', 'Message') 20 | 21 | def __init__(self, prefix, db=None): 22 | if not prefix.endswith(':'): 23 | prefix = "%s:" % prefix 24 | epics.Device.__init__(self, prefix, delim='', 25 | attrs=self.attrs) 26 | 27 | self._moving = False 28 | self._request = {} 29 | self._inst = None 30 | self.add_callback('InstName', self.OnInstName) 31 | self.add_callback('PosName', self.OnPosName) 32 | self.add_callback('Move', self.OnMove) 33 | 34 | def MoveDone(self): 35 | self.put('Message', 'Move complete') 36 | self.put('Move', 0) 37 | self._moving = False 38 | 39 | def OnMove(self, pvname=None, value=None, **kw): 40 | self._request['Move'] = value==1 41 | 42 | def OnPosName(self, pvname=None, value=None, **kw): 43 | self._request['Pos'] = value 44 | 45 | def OnInstName(self, pvname=None, value=None, **kw): 46 | self._request['Inst'] = value 47 | 48 | def SetTimeStamp(self): 49 | "set timestamp" 50 | self.put('TSTAMP', time.strftime("%Y-%b-%d %H:%M:%S")) 51 | 52 | def SetInfo(self, message=''): 53 | self.put('Info', str(message)) 54 | 55 | def Start(self, message='Starting'): 56 | "set timestamp" 57 | self.put('Info', str(message)) 58 | self.put('Move', 0) 59 | time.sleep(0.1) 60 | self.OnInstName(value=self.get('InstName')) 61 | self.OnPosName(value=self.get('PosName')) 62 | 63 | def Shutdown(self): 64 | "set timestamp" 65 | self.put('Info', 'offline') 66 | self.put('Message', '') 67 | self.put('InstOK', 0) 68 | self.put('PosOK', 0) 69 | self.put('Move', 0) 70 | -------------------------------------------------------------------------------- /epicsapps/instruments/pvconnector.py: -------------------------------------------------------------------------------- 1 | import time 2 | import wx 3 | import epics 4 | from epics.wx import EpicsFunction, DelayedEpicsCallback 5 | from epics import Motor 6 | 7 | from .utils import MOTOR_FIELDS, normalize_pvname 8 | 9 | class PVNameCtrl(wx.TextCtrl): 10 | """Text Control for an Epics PV that should try to be connected. 11 | this must be used with a EpicsPVList 12 | 13 | on <> or <>, this tries to connect to the 14 | PV named in the widget. If provided, the action provided is run. 15 | """ 16 | def __init__(self, panel, value='', pvlist=None, action=None, **kws): 17 | self.pvlist = pvlist 18 | self.action = action 19 | wx.TextCtrl.__init__(self, panel, wx.ID_ANY, value=value, **kws) 20 | self.Bind(wx.EVT_CHAR, self.onChar) 21 | self.Bind(wx.EVT_KILL_FOCUS, self.onFocus) 22 | 23 | def onFocus(self, evt=None): 24 | if self.pvlist is not None: 25 | self.pvlist.connect_pv(self.Value, action=self.action, 26 | wid=self.GetId()) 27 | evt.Skip() 28 | 29 | def onChar(self, event): 30 | key = event.GetKeyCode() 31 | value = wx.TextCtrl.GetValue(self).strip() 32 | pos = wx.TextCtrl.GetSelection(self) 33 | if key == wx.WXK_RETURN and self.pvlist is not None: 34 | self.pvlist.connect_pv(value, action=self.action, 35 | wid=self.GetId()) 36 | event.Skip() 37 | 38 | class EpicsPVList(object): 39 | """a wx class to hold a list of PVs, and 40 | handle the connection of new PVs. 41 | 42 | The main attribute is '.pvs', a dictionary of PVs, with 43 | pvname keys. 44 | 45 | The main way to use this is with the PVNameCtrl above 46 | 47 | """ 48 | def __init__(self, parent, timeout=10): 49 | self.pvs = {} 50 | self.in_progress = {} 51 | self.timeout = timeout 52 | self.etimer = wx.Timer(parent) 53 | parent.Bind(wx.EVT_TIMER, self.onTimer, self.etimer) 54 | self.need_connecting = [] 55 | 56 | def onTimer(self, event=None): 57 | "timer event handler: looks for in_progress, may timeout" 58 | time.sleep(0.01) 59 | if len(self.in_progress) == 0: 60 | return 61 | for pvname in self.in_progress: 62 | wid, action, xtime = self.in_progress[pvname] 63 | if action is not None: 64 | self.__connect(pvname) 65 | if time.time() - xtime > self.timeout: 66 | self.in_progress.pop(pvname) 67 | 68 | def init_connect(self, pvname, is_motor=False): 69 | """try to connect epics PV, executing 70 | action(wid=wid, pvname=pvname, pv=pv) 71 | """ 72 | if pvname is None or len(pvname) < 1: 73 | return 74 | pvname = normalize_pvname(pvname) 75 | 76 | if pvname in self.pvs: 77 | return 78 | self.pvs[pvname] = epics.get_pv(pvname, form='native') 79 | self.in_progress[pvname] = (None, None, time.time()) 80 | if is_motor: 81 | prefix = pvname.replace('.VAL', '') 82 | for field in MOTOR_FIELDS: 83 | fname = f"{prefix}{field}" 84 | self.pvs[fname] = epics.get_pv(fname, form='native') 85 | self.in_progress[fname] = (None, None, time.time()) 86 | 87 | def show_unconnected(self): 88 | all = 0 89 | unconn = [] 90 | for name, pv in self.pvs.items(): 91 | all += 1 92 | if not pv.connected: 93 | unconn.append(name) 94 | time.sleep(0.002) 95 | print("%d unconnected PVs of %d total" % (len(unconn), all)) 96 | s = '' 97 | for n in unconn: 98 | if len(s) > 1: 99 | s = "%s, %s" % (s, n) 100 | else: 101 | s = n 102 | if len(s) > 60: 103 | print(" %s" % s) 104 | s = '' 105 | if len(s) > 0: 106 | print(" %s" % s) 107 | 108 | 109 | # @EpicsFunction 110 | def connect_pv(self, pvname, is_motor=False, wid=None, action=None): 111 | """try to connect epics PV, executing 112 | action(wid=wid, pvname=pvname, pv=pv) 113 | """ 114 | # print(" connect_pv " , pvname) 115 | if pvname is None or len(pvname) < 1: 116 | return 117 | if '.' not in pvname: 118 | pvname = '%s.VAL' % pvname 119 | pvname = str(pvname) 120 | if pvname in self.pvs: 121 | return 122 | if pvname not in self.pvs: 123 | self.pvs[pvname] = epics.get_pv(pvname, form='native', timeout=1.0) 124 | self.in_progress[pvname] = (wid, action, time.time()) 125 | # if is_motor: 126 | # idot = pvname.find('.') 127 | # basname = pvname[:idot] 128 | # for ext in MOTOR_FIELDS: 129 | # pvname = "%s%s" % (basname, ext) 130 | # self.pvs[pvname] = epics.get_pv(pvname) 131 | # self.in_progress[pvname] = (wid, action, time.time()) 132 | 133 | @EpicsFunction 134 | def add_pv(self, pv, wid=None, action=None): 135 | """add an already connected PV to the pvlist""" 136 | if isinstance(pv, epics.PV) and pv not in self.pvs: 137 | self.pvs[pv.pvname] = pv 138 | 139 | @EpicsFunction 140 | def __connect(self, pvname): 141 | """if a new epics PV has connected, run the requested action""" 142 | 143 | if pvname not in self.pvs: 144 | self.pvs[pvname] = epics.get_pv(pvname, form='native', timeout=2.0) 145 | pv = self.pvs[pvname] 146 | if not self.pvs[pvname].connected: 147 | return 148 | 149 | try: 150 | wid, action, itime = self.in_progress.pop(pvname) 151 | except KeyError: 152 | wid, action, itime = None, None, 0 153 | pv.get_ctrlvars() 154 | 155 | if hasattr(action, '__call__'): 156 | action(wid=wid, pvname=pvname, pv=self.pvs[pvname]) 157 | -------------------------------------------------------------------------------- /epicsapps/instruments/test.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from epics import caget 3 | import time 4 | import instrument 5 | 6 | db = instrument.InstrumentDB('Test.ein') 7 | 8 | pvlist = ['13XRM:m1.VAL','13XRM:m2.VAL','13XRM:m3.VAL'] 9 | 10 | iname = 'Sample Stage' 11 | 12 | inst = db.get_instrument(iname) 13 | if inst is None: 14 | inst = db.add_instrument(iname, pvs=pvlist) 15 | 16 | inst = db.get_instrument(iname) 17 | print( inst, inst.pvs, inst.positions) 18 | 19 | for p in inst.pvs: 20 | print( p, p.pvtype) 21 | 22 | 23 | x = db.add_pv('13XRM:m4.VAL') 24 | print( 'Add PV ', x) 25 | 26 | print( inst.pvs) 27 | 28 | print( '== And now ==') 29 | for p in inst.pvs: 30 | print( p, p.pvtype) 31 | 32 | print( 'Instid = ', inst.id) 33 | IPV = instrument.Instrument_PV 34 | pvs_ordered = db.query(IPV).filter(IPV.instrument_id==inst.id 35 | ).order_by(IPV.display_order 36 | ).all() 37 | for i, pv in enumerate(inst.pvs): 38 | print( pv, pv.id) 39 | for o in pvs_ordered: 40 | if o.pv == pv: 41 | o.display_order = i+1 42 | 43 | db.commit() 44 | 45 | # 46 | # for ix, entry in enumerate(pvs_ordered): 47 | # print( entry.pv, entry.display_order 48 | # # entry.display_order = ix 49 | # ; 50 | # db.commit() 51 | 52 | 53 | # db.close() 54 | # time.sleep(0.1) 55 | # print( '=------------------------------------') 56 | # db = instrument.InstrumentDB('Bob.einst') 57 | # inst = db.get_instrument(iname) 58 | # print( inst, inst.pvs) 59 | # print( inst.positions) 60 | # ; 61 | #values = {} 62 | #for pv in pvlist: 63 | # values[pv] = 0 64 | # # 65 | #print( '====== ', inst.positions) 66 | #db.remove_position('p3', 'sample_stage') 67 | 68 | #print( db.get_info('modify_date')) 69 | 70 | #db.set_info('verify_erase', '1') 71 | #db.set_info('verify_move', '1') 72 | 73 | print( db.get_info('verify_erase')) 74 | 75 | # print( a, a.pvs) 76 | # for p in a.pvs: 77 | # print( p, p.pv.name, p.value) 78 | # ;# 79 | # values = {} 80 | # for pv in pvlist: 81 | # values[pv] = caget(pv) 82 | # 83 | # db.save_position('Current', inst, values) 84 | # 85 | # 86 | # print( '=====') 87 | # origin = db.get_position('Current') 88 | # 89 | # print( origin, origin.pvs) 90 | # ; 91 | -------------------------------------------------------------------------------- /epicsapps/instruments/upgrades.py: -------------------------------------------------------------------------------- 1 | ## 2 | ## code to upgrade sql schema / etc 3 | 4 | sqlcode = {} 5 | 6 | # version 1.2 -- 7 | # makes position (name, instrument_id) unique 8 | 9 | sqlcode['1.2'] = [ 10 | "begin transaction", 11 | """create temporary table position_backup( 12 | id INTEGER NOT NULL, name TEXT NOT NULL, 13 | notes TEXT, attributes TEXT, date DATETIME, 14 | instrument_id INTEGER, 15 | PRIMARY KEY (id), 16 | FOREIGN KEY(instrument_id) REFERENCES instrument (id))""", 17 | "insert into position_backup SELECT * from position", 18 | "drop table position", 19 | """create table position( 20 | id INTEGER NOT NULL, name TEXT NOT NULL, 21 | notes TEXT, attributes TEXT, date DATETIME, 22 | instrument_id INTEGER, 23 | unique(name, instrument_id), 24 | PRIMARY KEY (id), 25 | FOREIGN KEY(instrument_id) REFERENCES instrument (id))""", 26 | "insert into position SELECT * from position_backup", 27 | "drop table position_backup"] 28 | 29 | # version 1.3 adds "move order" to instruments to allow 30 | # controlling of the order that PVs are moved. 31 | sqlcode['1.3'] = ["alter table instrument_pv add column move_order integer;", 32 | "update instrument_pv set move_order=1;", 33 | ] 34 | 35 | # version 1.4 adds "modify_time" to position 36 | sqlcode['1.4'] = ["alter table instrument add column modify_time DATETIME;"] 37 | -------------------------------------------------------------------------------- /epicsapps/ionchamber/__init__.py: -------------------------------------------------------------------------------- 1 | from .ionchamber import IonChamber, start_ionchamber 2 | -------------------------------------------------------------------------------- /epicsapps/ionchamber/iocApp/IonChamber.db: -------------------------------------------------------------------------------- 1 | # This database contains fields used to calculate ion chamber currents 2 | grecord(stringin,"$(P)$(Q):AmpPV") { 3 | field(DESC, "IonChamber SRS570 Amp") 4 | field(VAL, "13IDC:A1") 5 | } 6 | 7 | grecord(stringin,"$(P)$(Q):EnergyPV") { 8 | field(DESC, "PV for Energy Value") 9 | field(VAL, "13IDA:E:E_RBV") 10 | } 11 | 12 | 13 | grecord(stringin,"$(P)$(Q):VoltPV") { 14 | field(DESC, "IonChamber Voltage PV") 15 | field(VAL, "") 16 | } 17 | 18 | grecord(ao,"$(P)$(Q):Volts") { 19 | field(DESC, "IonChamber Voltage") 20 | field(VAL, "") 21 | } 22 | 23 | grecord(ao,"$(P)$(Q):Energy") { 24 | field(DESC,"x-ray energy (eV)") 25 | field(PREC, "2") 26 | field(VAL, "10000") 27 | } 28 | 29 | grecord(stringin,"$(P)$(Q):Desc") { 30 | field(DESC, "Description") 31 | field(VAL, "Ion Chamber 1") 32 | } 33 | 34 | grecord(ao,"$(P)$(Q):Length") { 35 | field(DESC,"IonChamber Length (cm)") 36 | field(PREC, "1") 37 | field(VAL, 10.0) 38 | } 39 | 40 | grecord(ao,"$(P)$(Q):AbsPercent") { 41 | field(DESC,"IonChamber absorbed") 42 | field(PREC,"2") 43 | field(VAL,"0") 44 | } 45 | 46 | grecord(mbbi,"$(P)$(Q):Gas") { 47 | field(DESC,"IonChamber Gas") 48 | field(VAL,"1") 49 | field(ZRVL,"0") 50 | field(ZRST,"Air") 51 | field(ONVL,"1") 52 | field(ONST,"N2") 53 | field(TWVL,"2") 54 | field(TWST,"He") 55 | field(THVL,"3") 56 | field(THST,"Ar") 57 | field(FRVL,"4") 58 | field(FRST,"Kr") 59 | } 60 | 61 | grecord(ao, "$(P)$(Q):Current") { 62 | field(DESC,"IonChamber Current (uA)") 63 | field(PREC,"5") 64 | field(VAL, 0.) 65 | } 66 | 67 | grecord(ao, "$(P)$(Q):FluxAbs") { 68 | field(DESC,"IonChamber Flux Absorbed (Hz)") 69 | field(PREC,"0") 70 | field(VAL, 0) 71 | } 72 | 73 | grecord(ao, "$(P)$(Q):FluxOut") { 74 | field(DESC,"IonChamber Flux Out (Hz)") 75 | field(PREC,"0") 76 | field(VAL, 0) 77 | } 78 | 79 | grecord(stringout, "$(P)$(Q):FluxOutS") { 80 | field(DESC,"IonChamber Flux Out (Hz)") 81 | field(VAL, "0") 82 | } 83 | 84 | grecord(stringout,"$(P)$(Q):TimeStamp") { 85 | field(DESC, "last update time") 86 | field(VAL, "") 87 | } 88 | -------------------------------------------------------------------------------- /epicsapps/ionchamber/iocApp/IonChamber.req: -------------------------------------------------------------------------------- 1 | $(P)$(Q):EnergyPV 2 | $(P)$(Q):AmpPV 3 | $(P)$(Q):VoltPV 4 | $(P)$(Q):Length 5 | $(P)$(Q):Gas 6 | -------------------------------------------------------------------------------- /epicsapps/ionchamber/iocApp/st.cmd: -------------------------------------------------------------------------------- 1 | dbLoadRecords("$(CARS)/CARSApp/Db/IonChamber.db","P=13XRM:,Q=ION") 2 | -------------------------------------------------------------------------------- /epicsapps/microscope/__init__.py: -------------------------------------------------------------------------------- 1 | from .microscope import MicroscopeApp 2 | -------------------------------------------------------------------------------- /epicsapps/microscope/calibrationframe.py: -------------------------------------------------------------------------------- 1 | import wx 2 | import wx.lib.colourselect as csel 3 | from wxutils import Button, pack, SimpleText, FloatCtrl 4 | 5 | CEN = wx.ALL|wx.GROW|wx.ALIGN_CENTER 6 | LEFT = wx.ALIGN_LEFT 7 | 8 | class CalibrationFrame(wx.Frame): 9 | """ calibration frame """ 10 | 11 | def __init__(self, calibrations, current, callback=None, **kws): 12 | wx.Frame.__init__(self, None, -1, style=wx.DEFAULT_FRAME_STYLE, **kws) 13 | self.calibrations = {} 14 | self.calibrations.update(calibrations) 15 | self.current = current 16 | self.callback = callback 17 | calnames= list(calibrations.keys()) 18 | calib = self.calibrations[current] 19 | panel = wx.Panel(self) 20 | self.SetFont(wx.Font(10, wx.SWISS, wx.NORMAL, wx.BOLD, 0, "")) 21 | 22 | ok_button = Button(panel, "Apply", action=self.onApply, size=(75, -1)) 23 | done_button = Button(panel, "Done", action=self.onClose, size=(75, -1)) 24 | 25 | self.calname = wx.ComboBox(panel, choices=calnames, value=current, 26 | size=(150, -1), style=wx.TE_PROCESS_ENTER) 27 | 28 | opts = dict(minval=-500.0, maxval=500.0, precision=4, size=(75, -1), 29 | action=self.onCalibXY) 30 | self.calib_x = FloatCtrl(panel, value=calib[0], **opts) 31 | self.calib_y = FloatCtrl(panel, value=calib[1], **opts) 32 | 33 | sizer = wx.GridBagSizer(10, 3) 34 | sizer.SetVGap(5) 35 | sizer.SetHGap(5) 36 | 37 | def txt(label, size=-1): 38 | return SimpleText(panel, label, size=(size, -1)) 39 | 40 | toplabel ="Calibration Settings: " 41 | 42 | sizer.Add(txt(" Calibration Settings"), (0, 0), (1, 4), LEFT, 3) 43 | sizer.Add(txt(" Calibration Name "), (1, 0), (1, 1), LEFT, 2) 44 | sizer.Add(txt(" X Size (mm):"), (1, 1), (1, 1), LEFT, 2) 45 | sizer.Add(txt(" Y Size (mm):"), (1, 2), (1, 1), LEFT, 2) 46 | sizer.Add(self.calname, (2, 0), (1, 1), LEFT, 2) 47 | sizer.Add(self.calib_x, (2, 1), (1, 1), LEFT, 2) 48 | sizer.Add(self.calib_y, (2, 2), (1, 1), LEFT, 2) 49 | 50 | sizer.Add(wx.StaticLine(panel, size=(3400, 2)), (3, 0), (1, 4), 51 | wx.ALL|wx.GROW|wx.ALIGN_CENTER, 1) 52 | 53 | sizer.Add(ok_button, (4, 0), (1, 1), wx.ALL|wx.GROW|wx.ALIGN_CENTER, 1) 54 | sizer.Add(done_button, (4, 1), (1, 1), wx.ALL|wx.GROW|wx.ALIGN_CENTER, 1) 55 | 56 | panel.SetSizer(sizer) 57 | sizer.Fit(panel) 58 | self.SetSize((400, 225)) 59 | self.calname.Bind(wx.EVT_COMBOBOX, self.onCalibSelection) 60 | self.calname.Bind(wx.EVT_TEXT_ENTER, self.onCalibSelection) 61 | 62 | 63 | self.Bind(wx.EVT_CLOSE, self.onClose) 64 | self.Show() 65 | self.Raise() 66 | 67 | def onCalibSelection(self, event=None, **kws): 68 | name = self.calname.GetValue() 69 | # print(" On Calib selection ", name) 70 | if name not in self.calibrations: 71 | x, y = self.calib_x.GetValue(), self.calib_y.GetValue() 72 | self.calibrations[name] = [float(x), float(y)] 73 | else: 74 | calib = self.calibrations[name] 75 | self.calib_x.SetValue(calib[0]) 76 | self.calib_y.SetValue(calib[1]) 77 | 78 | def onCalibXY(self, event=None, **kws): 79 | try: 80 | name = self.calname.GetValue() 81 | x, y = self.calib_x.GetValue(), self.calib_y.GetValue() 82 | self.calibrations[name] = (x, y) 83 | except AttributeError: 84 | pass 85 | 86 | def onApply(self, event=None): 87 | if callable(self.callback): 88 | self.callback(self.calibrations, self.calname.GetValue()) 89 | 90 | def onClose(self, event=None): 91 | self.Destroy() 92 | -------------------------------------------------------------------------------- /epicsapps/microscope/configfile.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from ..utils import ConfigFile, load_yaml, get_default_configfile 4 | 5 | 6 | _configtext = """ 7 | title: 'SampleStage' 8 | workdir: /home/user 9 | verify_move: true 10 | verify_erase: true 11 | verify_overwrite: true 12 | center_with_fine_stages: false 13 | image_folder: Sample_Images 14 | zmq_push: true 15 | 16 | camera_type: areadetector 17 | camera_id: 1 18 | ad_prefix: 'XXIDEPS1:' 19 | ad_format: JPEG 20 | web_url: http://164.54.160.115/jpg/2/image.jpg 21 | 22 | calibration: 23 | - ['default', 1.0, 1.0] 24 | 25 | overlays: 26 | - ['scalebar', 100.0, 0.85, 0.97, 2.0, 255, 255, 128] 27 | - ['circle ', 10.0, 0.50, 0.50, 2.0, 0, 255, 0] 28 | 29 | scandb_credentials: ESCAN_CREDENTIALS 30 | instrument: SampleStage 31 | dbname: None 32 | xyzmotors: ['13XRM:m4.VAL', '13XRM:m5.VAL', '13XRM:m6.VAL'] 33 | offline_instrument: 34 | offline_xyzmotors: [] 35 | 36 | stages: 37 | - ['13XRM:m4.VAL', 'Coarse Stages', x, -1, 4, 10.5, 1] 38 | - ['13XRM:m5.VAL', 'Coarse Stages', y, -1, 4, 10.5, 1] 39 | - ['13XRM:m6.VAL', 'Coarse Stages', z, -1, 4, 10.5, 1] 40 | """ 41 | 42 | CONFFILE = 'microscope.yaml' 43 | 44 | class MicroscopeConfig(ConfigFile): 45 | def __init__(self, fname='microscope.yaml', default_config=None): 46 | if default_config is None: 47 | default_config = load_yaml(_configtext) 48 | ConfigFile.__init__(self, fname, default_config=default_config) 49 | -------------------------------------------------------------------------------- /epicsapps/microscope/fly2_camera.py: -------------------------------------------------------------------------------- 1 | import time 2 | from PIL import Image 3 | import numpy as np 4 | 5 | import wx 6 | 7 | try: 8 | import PyCapture2 9 | except ImportError: 10 | print("Cannot load PyCapture2 library") 11 | 12 | def map_attrs(object): 13 | out = {} 14 | for attr in dir(object): 15 | if attr.startswith('_'): 16 | continue 17 | out[attr.lower()] = getattr(object, attr) 18 | return out 19 | 20 | PYCAP2_PROPERTIES = map_attrs(PyCapture2.PROPERTY_TYPE) 21 | PYCAP2_FILEFORMATS = map_attrs(PyCapture2.IMAGE_FILE_FORMAT) 22 | 23 | def unbool(val): 24 | return {True:1, False: 0}[bool(val)] 25 | 26 | class Fly2Camera(object): 27 | """PyCapture 2Camera object""" 28 | def __init__(self, camera_id=0, bus_manager=None): 29 | if bus_manager is None: 30 | bus_manager = PyCapture2.BusManager() 31 | self.busman = bus_manager 32 | 33 | if camera_id is not None: 34 | self.Connect(camera_id=camera_id) 35 | 36 | def Connect(self, camera_id=0): 37 | self.cam = PyCapture2.Camera() 38 | self.cam.connect(self.busman.getCameraFromIndex(camera_id)) 39 | self.info = self.cam.getCameraInfo() 40 | 41 | def StartCapture(self): 42 | """""" 43 | self.cam.startCapture() 44 | 45 | def StopCapture(self): 46 | """""" 47 | self.cam.stopCapture() 48 | 49 | def GetProperty(self, name): 50 | if name not in PYCAP2_PROPERTIES: 51 | return None 52 | return self.cam.getProperty(PYCAP2_PROPERTIES[name]) 53 | 54 | def GetPropertyDict(self, name): 55 | p = self.getProperty(name) 56 | return {"type": p.type, 57 | "present": bool(p.present), 58 | "autoManualMode": bool(p.autoManualMode), 59 | "absControl": bool(p.absControl), 60 | "onOff": bool(p.onOff), 61 | "onePush": bool(p.onePush), 62 | "absValue": p.absValue, 63 | "valueA": p.valueA, 64 | "valueB": p.valueB} 65 | 66 | def SetPropertyValue(self, name, value, auto=False, absolute=True): 67 | """Set Value for property. Supports setting the properties 68 | 'brightness', 'sharpness', 'hue', 'saturation', 'gamma', 69 | 'shutter', 'gain', and 'white_balance' (see note below). 70 | 71 | Arguments 72 | --------- 73 | name property name ('gamma', 'gain', 'shutter', ...) 74 | value value for property 75 | absolute whether value is absolute (physical units) default=True 76 | auto whether to set autoManualMode default=False 77 | 78 | Example 79 | ------- 80 | context = pyfly2.Context() 81 | camera = context.get_camera(0) 82 | camera.Connect() 83 | camera.SetProperty('gain', 2.0) 84 | 85 | Notes 86 | ------ 87 | The 'white_balance' property takes a two-element tuple for values 88 | for red- and blue-white balance. Absolute control is forced to be 89 | False. To set the white balance, use something like 90 | 91 | camera.SetProperty('white_balance', (550, 800)) 92 | """ 93 | if name not in PYCAP2_PROPERTIES: 94 | return None 95 | 96 | prop = self.cam.getProperty(PYCAP2_PROPERTIES[name]) 97 | 98 | prop.onOff = 1 99 | prop.autoManualMode = unbool(auto) 100 | 101 | if name == 'white_balance': 102 | prop.valueA = value[0] 103 | prop.valueB = value[1] 104 | prop.absValue = 0.0 105 | prop.absControl = 0 106 | else: 107 | prop.absControl = unbool(absolute) 108 | prop.absValue = value 109 | self.cam.setProperty(prop) 110 | 111 | def SaveImageFile(self, filename, format="png"): 112 | """save image to disk""" 113 | img = self.cam.retrieveBuffer().convert(PyCapture2.PIXEL_FORMAT.RGB) 114 | img.save(filename, PYCAP2_FILEFORMATS[format]) 115 | 116 | def GetSize(self): 117 | """returns image size""" 118 | img = self.cam.retrieveBuffer() 119 | return (img.getCols(), img.getRows()) 120 | 121 | def GrabColor(self, format='rgb'): 122 | img = self.cam.retrieveBuffer() 123 | if format == 'bgr': 124 | img = img.convert(PyCapture2.PIXEL_FORMAT.BGR) 125 | elif format == 'rgb': 126 | img = img.convert(PyCapture2.PIXEL_FORMAT.RGB) 127 | 128 | return img 129 | 130 | def GrabNumPyImage(self, format='rgb'): 131 | """return an image as a NumPy array 132 | optionally specifying color 133 | """ 134 | img = self.cam.retrieveBuffer() 135 | ncols, nrows = img.getCols(), img.getRows() 136 | size = ncols * nrows 137 | shape = (ncols, nrows) 138 | if format == 'bgr': 139 | shape = (ncols, nrows, 3) 140 | img = img.convert(PyCapture2.PIXEL_FORMAT.BGR) 141 | elif format == 'rgb': 142 | shape = (ncols, nrows, 3) 143 | img = img.convert(PyCapture2.PIXEL_FORMAT.RGB) 144 | 145 | return np.array(img.getData()).reshape(shape) 146 | 147 | def GrabWxImage(self, scale=1.00, rgb=True, quality=wx.IMAGE_QUALITY_HIGH): 148 | """returns a wximage 149 | optionally specifying scale and color 150 | """ 151 | img = self.cam.retrieveBuffer() 152 | ncols, nrows = img.getCols(), img.getRows() 153 | scale = max(scale, 0.05) 154 | width, height = int(scale*ncols), int(scale*nrows) 155 | if rgb: 156 | img = img.convert(PyCapture2.PIXEL_FORMAT.RGB) 157 | 158 | return wx.Image(ncols, nrows, np.array(img.getData())).Rescale(width, height, 159 | quality=quality) 160 | def GrabPILImage(self): 161 | """""" 162 | # We import PIL here so that PIL is only a requirement if you need PIL 163 | img = self.cam.retrieveBuffer() 164 | ncols, nrows = img.getCols(), img.getRows() 165 | return Image.frombytes('L', (ncols, nrows), img.getData()) 166 | -------------------------------------------------------------------------------- /epicsapps/microscope/html2pos.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # convert SampleStage.html or Microscope.html to 4 | # Positions file 5 | 6 | import sys 7 | import json 8 | import HTMLParser 9 | from collections import OrderedDict 10 | 11 | class html2pos(HTMLParser.HTMLParser): 12 | def __init__(self): 13 | HTMLParser.HTMLParser.__init__(self) 14 | self.rows = [] 15 | self.row = OrderedDict() 16 | self.key = None 17 | self.in_td = False 18 | self.in_ahref = False 19 | 20 | def handle_starttag(self, tag, attrs): 21 | if tag == 'td': 22 | self.in_td = True 23 | elif tag == 'a': 24 | self.in_ahref = True 25 | self.href = attrs[0][1] 26 | elif tag == 'table': 27 | self.row = OrderedDict() 28 | 29 | def handle_endtag(self, tag): 30 | if tag == 'td': 31 | self.in_td = False 32 | elif tag == 'a': 33 | self.in_ahref = False 34 | 35 | elif tag == 'table': 36 | if 'Position:' in self.row: 37 | name = self.row.pop('Position:') 38 | extra = {'Date:': self.row.pop('Date')} 39 | extra['image'] = self.href 40 | data = list(self.row.items()) 41 | self.rows.append(json.dumps([name, data, extra])) 42 | self.row = OrderedDict() 43 | 44 | def handle_data(self, data): 45 | if self.in_td: 46 | dat = data.strip().replace('\t',' ').replace('\n','').replace('\r','') 47 | if self.key is not None: 48 | self.row[self.key] = dat 49 | if self.key.startswith('Position'): 50 | self.key = 'Date' 51 | else: 52 | self.key = None 53 | if dat.startswith('Position') or dat.startswith('13XRM'): 54 | self.key = dat 55 | if self.in_ahref: 56 | self.row['href'] = self.href 57 | 58 | if __name__ == "__main__": 59 | for htmlfile in sys.argv[1:]: 60 | outfile = htmlfile.replace('.html', '.pos') 61 | parser = html2pos() 62 | fh = open(htmlfile, 'r') 63 | parser.feed(fh.read()) 64 | fh.close() 65 | 66 | buff = ["#SampleViewer POSITIONS FILE v1.0"] 67 | buff.extend( parser.rows) 68 | buff.append('') 69 | fh = open(outfile, 'w') 70 | fh.write("\n".join(buff)) 71 | fh.close() 72 | -------------------------------------------------------------------------------- /epicsapps/microscope/imageframe.py: -------------------------------------------------------------------------------- 1 | import wx 2 | import base64 3 | import array 4 | 5 | 6 | class ImageDisplayFrame(wx.Frame): 7 | """ basic frame for displaying image 8 | 9 | """ 10 | iw, ih, pad = 700, 550, 15 11 | def __init__(self, **kws): 12 | wx.Frame.__init__(self, None, -1, 13 | style=wx.DEFAULT_FRAME_STYLE, **kws) 14 | self.wximage = None 15 | iw, ih = self.iw-self.pad, self.ih-self.pad 16 | panel = wx.Panel(self) 17 | self.label = wx.StaticText(panel, -1, " "*200, size=(300, -1)) 18 | 19 | self.bitmap = wx.StaticBitmap(panel, -1, 20 | wx.BitmapFromBuffer(iw, ih, 21 | array.array('B', [220]*3*iw*ih))) 22 | 23 | self.Bind(wx.EVT_SIZE, self.onSize) 24 | self.Bind(wx.EVT_CLOSE, self.onClose) 25 | 26 | sizer = wx.BoxSizer(wx.VERTICAL) 27 | 28 | sizer.Add(self.bitmap, 1, wx.ALL|wx.ALIGN_CENTER, 3) 29 | sizer.Add(self.label, 0, wx.ALL|wx.ALIGN_CENTER, 2) 30 | 31 | panel.SetSizer(sizer) 32 | s = self.GetBestSize() 33 | self.SetMinSize((iw, ih)) 34 | sizer.Fit(panel) 35 | self.Show() 36 | self.Raise() 37 | wx.CallAfter(self.SetSize, (s[0]+10, s[1]+75)) 38 | 39 | def showfile(self, fname, title=None, label=None): 40 | if title is not None: 41 | self.SetTitle(title) 42 | if label is not None: 43 | self.label.SetLabel(label) 44 | self.wximage = wx.Image(fname, wx.BITMAP_TYPE_ANY) 45 | self.show_bmp(self.wximage) 46 | 47 | def showb64img(self, data, title=None, label=None): 48 | imdata = base64.b64decode(data) 49 | if title is not None: 50 | self.SetTitle(title) 51 | if label is not None: 52 | self.label.SetLabel(label) 53 | self.wximage = wx.EmptyImage(self.iw-self.pad, self.ih-self.pad) 54 | self.wximage.SetData(imdata) 55 | self.show_bmp(self.wximage) 56 | 57 | def onSize(self, evt): 58 | self.iw, self.ih = evt.GetSize() 59 | if self.wximage is not None: 60 | self.show_bmp(self.wximage) 61 | evt.Skip() 62 | 63 | def show_bmp(self, img): 64 | winw, winh = self.GetSize() 65 | winw = max(winw, 600) 66 | winh = max(winh, 400) 67 | imgw, imgh = img.GetSize() 68 | scale = min((winw*1.0 - self.pad)/imgw, (winh*1.0 - self.pad)/imgh) 69 | ximg = img.Rescale(winw-self.pad, winh-self.pad) 70 | self.bitmap.SetBitmap(wx.Bitmap(ximg)) 71 | self.Refresh() 72 | 73 | def onClose(self, event=None): 74 | self.Destroy() 75 | -------------------------------------------------------------------------------- /epicsapps/microscope/imagepanel_epicsarray.py: -------------------------------------------------------------------------------- 1 | """Image Panel for Epics AreaDetector 2 | """ 3 | 4 | import wx 5 | import time 6 | import os 7 | import numpy as np 8 | from functools import partial 9 | from epics import PV, Device, caput, poll 10 | 11 | from epics.wx import DelayedEpicsCallback, EpicsFunction 12 | from epics.wx import PVEnumChoice, PVFloatCtrl, PVTextCtrl 13 | 14 | from PIL import Image 15 | 16 | from .imagepanel_base import ImagePanel_Base, ConfPanel_Base 17 | 18 | from wxutils import pack, Button 19 | 20 | class ImagePanel_EpicsArray(ImagePanel_Base): 21 | img_attrs = ('ArrayData', 'UniqueId_RBV', 'NDimensions_RBV', 22 | 'ArraySize0_RBV', 'ArraySize1_RBV', 'ArraySize2_RBV', 23 | 'ColorMode_RBV', 'RequestTStamp', 'PublishTStamp') 24 | 25 | """Image Panel for Simple Array pushed by PushEpics""" 26 | def __init__(self, parent, prefix=None, format='JPEG', 27 | writer=None, autosave_file=None, **kws): 28 | ImagePanel_Base.__init__(self, parent, -1, 29 | size=(800, 600), 30 | writer=writer, 31 | autosave_file=autosave_file, **kws) 32 | 33 | self.format = format 34 | self.set_prefix(prefix) 35 | self.imgcount = 0 36 | self.imgcount_start = 0 37 | self.last_update = 0.0 38 | 39 | self.timer = wx.Timer(self) 40 | self.Bind(wx.EVT_TIMER, self.onTimer, self.timer) 41 | 42 | def set_prefix(self, prefix): 43 | if prefix.endswith(':'): prefix = prefix[:-1] 44 | if prefix.endswith(':image1'): prefix = prefix[:-7] 45 | self.prefix = prefix 46 | 47 | self.ad_img = Device(prefix + ':image1:', delim='', 48 | attrs=self.img_attrs) 49 | w, h = self.GetImageSize() 50 | self.cam_name = prefix 51 | 52 | def config_filesaver(self, prefix, format): 53 | if not prefix.endswith(':'): prefix = "%s:" % prefix 54 | if not format.endswith('1'): format = "%s1" % format 55 | if not format.endswith('1:'): format = "%s:" % format 56 | pass 57 | 58 | def Start(self): 59 | "turn camera on" 60 | self.timer.Start(50) 61 | if self.autosave_thread is not None: 62 | self.autosave_thread.start() 63 | 64 | def Stop(self): 65 | "turn camera off" 66 | self.timer.Stop() 67 | self.autosave = False 68 | if self.autosave_thread is not None: 69 | self.autosave_thread.join() 70 | 71 | def GetImageSize(self): 72 | arrsize0 = self.ad_img.ArraySize0_RBV 73 | arrsize1 = self.ad_img.ArraySize1_RBV 74 | arrsize2 = self.ad_img.ArraySize2_RBV 75 | self.arrsize = (arrsize0, arrsize1, arrsize2) 76 | self.colormode = self.ad_img.ColorMode_RBV 77 | 78 | w, h = arrsize0, arrsize1 79 | if self.ad_img.NDimensions_RBV == 3: 80 | w, h = arrsize1, arrsize2 81 | self.img_w = float(w+0.5) 82 | self.img_h = float(h+0.5) 83 | return w, h 84 | 85 | def GrabNumpyImage(self): 86 | width, height = self.GetImageSize() 87 | 88 | im_mode = 'L' 89 | self.im_size = (self.arrsize[0], self.arrsize[1]) 90 | if self.ad_img.ColorMode_RBV == 2: 91 | im_mode = 'RGB' 92 | self.im_size = (self.arrsize[1], self.arrsize[2]) 93 | 94 | dcount = self.arrsize[0] * self.arrsize[1] 95 | if self.ad_img.NDimensions_RBV == 3: 96 | dcount *= self.arrsize[2] 97 | 98 | img = self.ad_img.PV('ArrayData').get(count=dcount) 99 | if img is None: 100 | time.sleep(0.025) 101 | img = self.ad_img.PV('ArrayData').get(count=dcount) 102 | 103 | if self.ad_img.ColorMode_RBV == 2: 104 | img = img.reshape((width, height, 3)).sum(axis=2) 105 | else: 106 | img = img.reshape((width, height)) 107 | return img 108 | 109 | def GrabWxImage(self, scale=1, rgb=True, can_skip=True): 110 | if self.ad_img is None: 111 | print('GrabWxImage .. no ad_img ', self.ad_img) 112 | return 113 | 114 | # imgcount = self.ad_cam.ArrayCounter_RBV 115 | now = time.time() 116 | if (can_skip and (imgcount == self.imgcount or 117 | abs(now - self.last_update) < 0.025)): 118 | return None 119 | rawdata = self.GetNumpyImage() 120 | if rawdata is None: 121 | return 122 | 123 | self.imgcount = imgcount 124 | self.last_update = time.time() 125 | 126 | if (self.ad_img.ColorMode_RBV == 0 and 127 | isinstance(rawdata, np.ndarray) and 128 | rawdata.dtype != np.uint8): 129 | im_mode = 'I' 130 | rawdata = rawdata.astype(np.uint32) 131 | if im_mode in ('L', 'I'): 132 | image = wx.EmptyImage(width, height) 133 | imbuff = Image.frombuffer(im_mode, self.im_size, rawdata, 134 | 'raw', im_mode, 0, 1) 135 | image.SetData(imbuff.convert('RGB').tobytes()) 136 | elif im_mode == 'RGB': 137 | rawdata.shape = (3, width, height) 138 | rawdata = rawdata.astype(np.uint8) 139 | image = wx.Image(width, height, rawdata) 140 | return image.Scale(int(scale*width), int(scale*height)) 141 | 142 | class ConfPanel_EpicsArray(ConfPanel_Base): 143 | def __init__(self, parent, url=None, center_cb=None, xhair_cb=None, **kws): 144 | super(ConfPanel_ZMQ, self).__init__(parent, center_cb=center_cb, 145 | xhair_cb=xhair_cb, 146 | size=(280, 300)) 147 | 148 | title = wx.StaticText(self, label="Epics Array", size=(285, 25)) 149 | 150 | sizer = self.sizer 151 | sizer.Add(title, (0, 0), (1, 3), wx.ALIGN_LEFT|wx.ALL) 152 | next_row = self.show_position_info(row=2) 153 | pack(self, sizer) 154 | -------------------------------------------------------------------------------- /epicsapps/microscope/imagepanel_weburl.py: -------------------------------------------------------------------------------- 1 | """Image Panel for Web cameras 2 | """ 3 | 4 | import wx 5 | import time 6 | import os 7 | import io 8 | import urllib 9 | import requests 10 | 11 | import numpy as np 12 | 13 | from PIL import Image 14 | 15 | from wxutils import pack 16 | 17 | from .imagepanel_base import ImagePanel_Base, ConfPanel_Base 18 | 19 | class ImagePanel_URL(ImagePanel_Base): 20 | """Image Panel for webcam""" 21 | def __init__(self, parent, url=None, writer=None, autosave_file=None, **kws): 22 | super(ImagePanel_URL, self).__init__(parent, -1, 23 | size=(800, 600), 24 | writer=writer, 25 | autosave_file=autosave_file, **kws) 26 | 27 | self.url = url 28 | pil_image = Image.open(self.read_url()) 29 | width, height = pil_image.size 30 | 31 | self.img_w = float(width+0.5) 32 | self.img_h = float(height+0.5) 33 | self.img_size = (width, height) 34 | self.cam_name = url 35 | self.imgcount = 0 36 | self.imgcount_start = 0 37 | self.last_update = 0.0 38 | self.timer = wx.Timer(self) 39 | self.Bind(wx.EVT_TIMER, self.onTimer, self.timer) 40 | 41 | def read_url(self): 42 | return io.BytesIO(requests.get(self.url).content) 43 | 44 | def Start(self): 45 | "turn camera on" 46 | self.timer.Start(65) 47 | #if self.autosave_thread is not None: 48 | # self.autosave_thread.start() 49 | 50 | def Stop(self): 51 | "turn camera off" 52 | self.timer.Stop() 53 | self.autosave = False 54 | 55 | def GrabNumpyImage(self): 56 | pimg = Image.open(self.read_url()) 57 | self.data = np.array(pimg.getdata()).reshape(pimg.size[0], pimg.size[1], 3) 58 | return self.data 59 | 60 | def GrabWxImage(self, scale=1, rgb=True, can_skip=True): 61 | try: 62 | wximage = wx.Image(self.read_url()) 63 | time.sleep(0.025) 64 | return wximage.Scale(int(scale*self.img_w), int(scale*self.img_h)) 65 | except: 66 | pass 67 | 68 | class ConfPanel_URL(ConfPanel_Base): 69 | def __init__(self, parent, url=None, center_cb=None, xhair_cb=None, **kws): 70 | super(ConfPanel_URL, self).__init__(parent, center_cb=center_cb, 71 | xhair_cb=xhair_cb, 72 | size=(280, 300)) 73 | 74 | super(ConfPanel_URL, self).__init__(parent, center_cb=center_cb, 75 | xhair_cb=xhair_cb, **kws) 76 | title = wx.StaticText(self, label="Webcam Config", size=(285, 25)) 77 | 78 | sizer = self.sizer # wx.BoxSizer(wx.VERTICAL) 79 | sizer.Add(title, (0, 0), (1, 3), wx.ALIGN_LEFT|wx.ALL) 80 | next_row = self.show_position_info(row=2) 81 | pack(self, sizer) 82 | -------------------------------------------------------------------------------- /epicsapps/microscope/imagepanel_zmqjpeg.py: -------------------------------------------------------------------------------- 1 | """ 2 | Image Panel for JPEG Images sent over ZeroMQ 3 | as published by the default ImagePanel 4 | """ 5 | 6 | import wx 7 | import time 8 | import os 9 | import sys 10 | import json 11 | import numpy as np 12 | import io 13 | import zmq 14 | import base64 15 | 16 | from PIL import Image 17 | 18 | from wxutils import pack 19 | 20 | from .imagepanel_base import ImagePanel_Base, ConfPanel_Base 21 | 22 | class ImagePanel_ZMQ(ImagePanel_Base): 23 | """Image Panel for JPEGs sent by ZeroMQ""" 24 | def __init__(self, parent, host=None, port=17166, **kws): 25 | 26 | super(ImagePanel_ZMQ, self).__init__(parent, -1, 27 | size=(800, 600), 28 | publish_jpeg=False) 29 | self.ctx = zmq.Context() 30 | self.connstr = "tcp://%s:%s" % (host, port) 31 | self.socket = self.ctx.socket(zmq.REQ) 32 | self.socket.setsockopt(zmq.SNDTIMEO, 500) 33 | self.socket.setsockopt(zmq.RCVTIMEO, 500) 34 | self.socket.setsockopt(zmq.LINGER, 500) 35 | self.socket.setsockopt(zmq.CONNECT_TIMEOUT, 500) 36 | self.socket.connect(self.connstr) 37 | self.connected = True 38 | self.last_image_time = -1 39 | self.timer = wx.Timer(self) 40 | self.Bind(wx.EVT_TIMER, self.onTimer, self.timer) 41 | 42 | self.Start() 43 | 44 | def connect(self): 45 | if self.host is None or self.port is None: 46 | return 47 | self.connected = True 48 | time.sleep(1) 49 | 50 | def Start(self): 51 | "turn camera on" 52 | self.timer.Start(100) 53 | 54 | def Stop(self): 55 | "turn camera off" 56 | self.timer.Stop() 57 | self.autosave = False 58 | 59 | def GrabWxImage(self, scale=1, rgb=True, can_skip=True): 60 | if not self.connected: 61 | return 62 | if time.time() - self.last_image_time < 0.25: 63 | return 64 | 65 | try: 66 | self.socket.send(b'send image') 67 | message = self.socket.recv() 68 | except: 69 | et, ev, tb = sys.exc_info() 70 | time.sleep(2) 71 | return None 72 | 73 | if message.startswith(b'jpeg:'): 74 | tmp = io.BytesIO() 75 | tmp.write(base64.b64decode(message[5:])) 76 | tmp.seek(0) 77 | wximage = wx.Image(tmp) 78 | self.last_image_time = time.time() 79 | return wximage.Scale(int(scale*self.img_w), int(scale*self.img_h)) 80 | 81 | class ConfPanel_ZMQ(ConfPanel_Base): 82 | def __init__(self, parent, url=None, center_cb=None, xhair_cb=None, **kws): 83 | super(ConfPanel_ZMQ, self).__init__(parent, center_cb=center_cb, 84 | xhair_cb=xhair_cb, 85 | size=(280, 300)) 86 | 87 | title = wx.StaticText(self, label="JPEG Config", size=(285, 25)) 88 | 89 | sizer = self.sizer 90 | sizer.Add(title, (0, 0), (1, 3), wx.ALIGN_LEFT|wx.ALL) 91 | next_row = self.show_position_info(row=2) 92 | pack(self, sizer) 93 | -------------------------------------------------------------------------------- /epicsapps/microscope/overlayframe.py: -------------------------------------------------------------------------------- 1 | import wx 2 | import wx.lib.colourselect as csel 3 | from wxutils import Button, pack, SimpleText, FloatCtrl 4 | 5 | CEN = wx.ALL|wx.GROW|wx.ALIGN_CENTER 6 | LEFT = wx.ALIGN_LEFT 7 | 8 | class OverlayFrame(wx.Frame): 9 | """ settings for overlays""" 10 | shapes = ('None', 'line', 'circle') 11 | 12 | def __init__(self, config=None, calib=(0.01, 0.01), 13 | callback=None, **kws): 14 | wx.Frame.__init__(self, None, -1, 15 | style=wx.DEFAULT_FRAME_STYLE, **kws) 16 | self.callback = callback 17 | self.config = config 18 | self.calib = calib 19 | self.wids = wids = [] 20 | 21 | panel = wx.Panel(self) 22 | self.SetFont(wx.Font(10, wx.SWISS, wx.NORMAL, wx.BOLD, 0, "")) 23 | 24 | ok_button = Button(panel, "Apply", action=self.onApply, size=(75, -1)) 25 | done_button = Button(panel, "Done", action=self.onClose, size=(75, -1)) 26 | 27 | sbar = circ = None 28 | for overlay in self.config.get('overlays', []): 29 | name = overlay[0].lower() 30 | if name == 'scalebar': 31 | sbar = [float(x) for x in overlay[1:]] 32 | elif name == 'circle': 33 | circ = [float(x) for x in overlay[1:]] 34 | 35 | ssiz, sx, sy, swid, scolr, scolg, scolb = sbar 36 | csiz, cx, cy, cwid, ccolr, ccolg, ccolb = circ 37 | 38 | scol = wx.Colour(int(scolr), int(scolg), int(scolb)) 39 | ccol = wx.Colour(int(ccolr), int(ccolg), int(ccolb)) 40 | 41 | opts = dict(minval=0, maxval=1, precision=3, size=(60, -1)) 42 | wopts = dict(minval=0, maxval=10, precision=1, size=(60, -1)) 43 | sopts = dict(minval=0, maxval=1000, precision=0, size=(60, -1)) 44 | popts = dict(minval=-1000, maxval=1000, precision=3, size=(60, -1)) 45 | 46 | self.scalebar_col = csel.ColourSelect(panel, -1, "", scol, size=(75, 25)) 47 | self.circle_col = csel.ColourSelect(panel, -1, "", ccol, size=(75, 25)) 48 | 49 | self.scalebar_size = FloatCtrl(panel, value=ssiz, **sopts) 50 | self.scalebar_x = FloatCtrl(panel, value=sx, **opts) 51 | self.scalebar_y = FloatCtrl(panel, value=sy, **opts) 52 | self.scalebar_wid = FloatCtrl(panel, value=swid, **wopts) 53 | 54 | 55 | self.circle_size = FloatCtrl(panel, value=csiz, **sopts) 56 | self.circle_x = FloatCtrl(panel, value=cx, **opts) 57 | self.circle_y = FloatCtrl(panel, value=cy, **opts) 58 | self.circle_wid = FloatCtrl(panel, value=cwid, **wopts) 59 | 60 | sizer = wx.GridBagSizer(10, 7) 61 | sizer.SetVGap(5) 62 | sizer.SetHGap(5) 63 | 64 | def txt(label, size=-1): 65 | return SimpleText(panel, label, size=(size, -1)) 66 | 67 | toplabel ="Configure Image Overlays: pixel size=%.2f \u03bCm" 68 | 69 | sizer.Add(txt(toplabel % (abs(calib[0]))), (0, 0), (1, 4), CEN, 3) 70 | sizer.Add(txt(" Object "), (1, 0), (1, 1), LEFT, 2) 71 | sizer.Add(txt("Color"), (1, 1), (1, 1), LEFT, 2) 72 | sizer.Add(txt("Size (\u03bCm)"), (1, 2), (1, 1), LEFT, 2) 73 | sizer.Add(txt("X (fraction)"), (1, 3), (1, 1), LEFT, 2) 74 | sizer.Add(txt("Y (fraction)"), (1, 4), (1, 1), LEFT, 2) 75 | sizer.Add(txt("Line width (pixels)"), (1, 5), (1, 1), LEFT, 2) 76 | 77 | sizer.Add(txt(" Scalebar "), (2, 0), (1, 1), LEFT, 2) 78 | sizer.Add(self.scalebar_col, (2, 1), (1, 1), CEN, 1) 79 | sizer.Add(self.scalebar_size, (2, 2), (1, 1), CEN, 1) 80 | sizer.Add(self.scalebar_x, (2, 3), (1, 1), CEN, 1) 81 | sizer.Add(self.scalebar_y, (2, 4), (1, 1), CEN, 1) 82 | sizer.Add(self.scalebar_wid, (2, 5), (1, 1), CEN, 1) 83 | 84 | sizer.Add(txt(" Target "), (3, 0), (1, 1), LEFT, 2) 85 | sizer.Add(self.circle_col, (3, 1), (1, 1), CEN, 1) 86 | sizer.Add(self.circle_size, (3, 2), (1, 1), CEN, 1) 87 | sizer.Add(self.circle_x, (3, 3), (1, 1), CEN, 1) 88 | sizer.Add(self.circle_y, (3, 4), (1, 1), CEN, 1) 89 | sizer.Add(self.circle_wid, (3, 5), (1, 1), CEN, 1) 90 | 91 | sizer.Add(wx.StaticLine(panel, size=(220, 2)), (4, 0), (1, 6), 92 | wx.ALL|wx.GROW|wx.ALIGN_CENTER, 1) 93 | 94 | sizer.Add(ok_button, (5, 0), (1, 1), wx.ALL|wx.GROW|wx.ALIGN_CENTER, 1) 95 | sizer.Add(done_button, (5, 1), (1, 1), wx.ALL|wx.GROW|wx.ALIGN_CENTER, 1) 96 | 97 | panel.SetSizer(sizer) 98 | sizer.Fit(panel) 99 | self.SetSize((500, 225)) 100 | self.Bind(wx.EVT_CLOSE, self.onClose) 101 | self.Show() 102 | self.Raise() 103 | 104 | def onApply(self, event=None): 105 | if not callable(self.callback): 106 | return 107 | scol = self.scalebar_col.GetColour() 108 | swid = self.scalebar_wid.GetValue() 109 | sx = self.scalebar_x.GetValue() 110 | sy = self.scalebar_y.GetValue() 111 | ssiz = self.scalebar_size.GetValue() 112 | 113 | ccol = self.circle_col.GetColour() 114 | cwid = self.circle_wid.GetValue() 115 | csiz = self.circle_size.GetValue() 116 | cx = self.circle_x.GetValue() 117 | cy = self.circle_y.GetValue() 118 | overlays = [['scalebar', ssiz, sx, sy, swid, scol[0], scol[1], scol[2]], 119 | ['circle', csiz, cx, cy, cwid, ccol[0], ccol[1], ccol[2]]] 120 | self.callback(overlays) 121 | 122 | def onClose(self, event=None): 123 | self.Destroy() 124 | -------------------------------------------------------------------------------- /epicsapps/pvlogger/__init__.py: -------------------------------------------------------------------------------- 1 | from .pvlogger import PVLogger 2 | from .logfile import read_logfile, read_logfolder 3 | from ..utils import HAS_WXPYTHON 4 | PVLoggerApp = None 5 | if HAS_WXPYTHON: 6 | from .pvlogger_app import PVLoggerApp 7 | -------------------------------------------------------------------------------- /epicsapps/pvlogger/configfile.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from ..utils import ConfigFile, load_yaml, get_default_configfile 4 | 5 | # default pvlogger configuration 6 | _configtext = """ 7 | datadir: "" 8 | folder: "pvlog" 9 | end_datetime: '' 10 | instruments: [] 11 | pvs: 12 | - S:SRcurrentAI.VAL | Storage Ring Current | 0.002 13 | """ 14 | 15 | CONFIGFILE = 'pvlog.yaml' 16 | 17 | class PVLoggerConfig(ConfigFile): 18 | def __init__(self, fname=CONFIGFILE, default_config=None): 19 | if default_config is None: 20 | default_config = load_yaml(_configtext) 21 | ConfigFile.__init__(self, fname, default_config=default_config) 22 | -------------------------------------------------------------------------------- /epicsapps/pvlogger/eventtableview.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import time 3 | from datetime import datetime 4 | 5 | import wx 6 | import wx.lib.scrolledpanel as scrolled 7 | import wx.lib.agw.flatnotebook as flat_nb 8 | import wx.lib.colourselect as csel 9 | import wx.dataview as dv 10 | 11 | from wxutils import (GridPanel, SimpleText, MenuItem, OkCancel, Popup, 12 | FileOpen, SavedParameterDialog, Font, FloatSpin, 13 | HLine, GUIColors, COLORS, Button, flatnotebook, 14 | Choice, FileSave, FileCheckList, LEFT, RIGHT, pack, 15 | FRAMESTYLE, LEFT) 16 | 17 | from wxmplot.colors import hexcolor 18 | 19 | from .logfile import TZONE 20 | 21 | DVSTYLE = dv.DV_SINGLE|dv.DV_VERT_RULES|dv.DV_ROW_LINES 22 | 23 | 24 | FNB_STYLE = flat_nb.FNB_NO_X_BUTTON 25 | FNB_STYLE |= flat_nb.FNB_SMART_TABS|flat_nb.FNB_NO_NAV_BUTTONS 26 | 27 | COLORS = ('#1f77b4', '#d62728', '#2ca02c', '#ff7f0e', '#9467bd', 28 | '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf') 29 | 30 | def dtformat(ts): 31 | dt = datetime.fromtimestamp(ts) 32 | return dt.isoformat(sep=' ', timespec='milliseconds') 33 | 34 | class EventDataModel(dv.DataViewIndexListModel): 35 | def __init__(self, event_data=None, dt1=None, dt2=None): 36 | dv.DataViewIndexListModel.__init__(self, 0) 37 | self.event_data = event_data 38 | self.pvcolors = {} 39 | self.dt1 = dt1 40 | self.dt2 = dt2 41 | self.set_data() 42 | 43 | def set_data(self, event_data=None, dt1=None, dt2=None): 44 | if event_data is not None: 45 | self.event_data = event_data 46 | if dt1 is not None: 47 | self.dt1 = dt1 48 | if dt2 is not None: 49 | self.dt2 = dt2 50 | self.data = [] 51 | self.pvcolors = {} 52 | if self.event_data is None or self.dt1 is None or self.dt2 is None: 53 | return 54 | tmin = self.dt1.timestamp() 55 | tmax = self.dt2.timestamp() 56 | ipv = 0 57 | for pvdesc, dat in self.event_data.items(): 58 | if pvdesc not in self.pvcolors: 59 | self.pvcolors[pvdesc] = COLORS[ipv] 60 | ipv = (ipv+1) % len(COLORS) 61 | for ts, cval in dat.events: 62 | if ts >= tmin and ts <= tmax: 63 | self.data.append([pvdesc, dat.pvname, dtformat(ts), cval]) 64 | self.data = sorted(self.data, key=lambda x: x[2]) 65 | self.Reset(len(self.data)) 66 | 67 | def ClearAll(self): 68 | for row in self.data: 69 | row[0] = False 70 | self.Reset(len(self.data)) 71 | 72 | def GetColumnType(self, col): 73 | return "string" 74 | 75 | def GetValueByRow(self, row, col): 76 | return self.data[row][col] 77 | 78 | def GetAttrByRow(self, row, col, attr): 79 | colour = self.pvcolors[self.data[row][0]] 80 | attr.SetColour(colour) 81 | return True 82 | 83 | def GetColumnCount(self): 84 | return self.ncols 85 | 86 | def GetCount(self): 87 | return len(self.data) 88 | 89 | 90 | class EventTablePanel(wx.Panel) : 91 | """View Table of Events for Multiple PVs""" 92 | def __init__(self, parent, size=(850, 400)): 93 | self.parent = parent 94 | wx.Panel.__init__(self, parent, -1, size=size) 95 | 96 | spanel = scrolled.ScrolledPanel(self, size=size) 97 | self.dvc = dv.DataViewCtrl(spanel, style=DVSTYLE) 98 | self.model = EventDataModel() 99 | self.dvc.AssociateModel(self.model) 100 | 101 | for icol, dat in enumerate((('PV Description ', 200), 102 | ('PV Name ', 175), 103 | ('Date/Time', 250), 104 | ('Value', 350))): 105 | title, width = dat 106 | self.dvc.AppendTextColumn(title, icol, width=width) 107 | col = self.dvc.Columns[icol] 108 | col.Alignment = wx.ALIGN_LEFT 109 | col.Sortable = True 110 | col.SetSortOrder(1) 111 | 112 | sizer = wx.BoxSizer(wx.VERTICAL) 113 | sizer.Add(self.dvc, 1, LEFT|wx.ALL|wx.GROW) 114 | pack(spanel, sizer) 115 | 116 | mainsizer = wx.BoxSizer(wx.VERTICAL) 117 | mainsizer.Add(spanel, 1, wx.GROW|wx.ALL, 5) 118 | pack(self, mainsizer) 119 | 120 | def set_data(self, event_data, dt1, dt2): 121 | self.model.set_data(event_data, dt1, dt2) 122 | 123 | def onClearAll(self, event=None): 124 | self.model.ClearAll() 125 | 126 | class EventTableFrame(wx.Frame) : 127 | """View Table of PV Values""" 128 | def __init__(self, parent=None, title='PVLogger Event Table View', 129 | size=(850, 500)): 130 | self.parent = parent 131 | wx.Frame.__init__(self, parent, -1, title=title, 132 | style=FRAMESTYLE, size=size) 133 | 134 | self.panel = EventTablePanel(parent=self) 135 | sizer = wx.BoxSizer(wx.VERTICAL) 136 | sizer.Add(self.panel, 1, LEFT|wx.GROW|wx.EXPAND, 3) 137 | pack(self, sizer) 138 | self.Show() 139 | self.Raise() 140 | 141 | def set_data(self, event_data, dt1, dt2): 142 | self.panel.set_data(event_data, dt1, dt2) 143 | -------------------------------------------------------------------------------- /epicsapps/stripchart/__init__.py: -------------------------------------------------------------------------------- 1 | from .stripchart import StripChartApp, StripChartFrame 2 | -------------------------------------------------------------------------------- /epicsapps/stripchart/configfile.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from ..utils import ConfigFile, load_yaml, get_default_configfile 4 | 5 | _configtext = """ 6 | workdir: "/home/user" 7 | ## pvname, pvdesc, use_log, ymin, ymax 8 | pvs: 9 | - ['S:SRcurrentAI.VAL', 'Storage Ring Current', 0, '', ''] 10 | """ 11 | 12 | CONFFILE = 'stripchart.yaml' 13 | 14 | class StripChartConfig(ConfigFile): 15 | def __init__(self, fname='stripchart.yaml', default_config=None): 16 | if default_config is None: 17 | default_config = load_yaml(_configtext) 18 | ConfigFile.__init__(self, fname, default_config=default_config) 19 | -------------------------------------------------------------------------------- /epicsapps/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from pyshortcuts import (debugtimer, fix_filename, new_filename, 2 | fix_varname, isotime) 3 | 4 | 5 | from .configfile import (ConfigFile, get_configfolder, 6 | get_default_configfile, load_yaml, 7 | read_recents_file, write_recents_file) 8 | 9 | from .utils import (get_pvtypes, get_pvdesc, normalize_pvname) 10 | from .textfile import read_textfile 11 | from .griddata import DataTableGrid, DictFrame 12 | 13 | HAS_WXPYTHON = False 14 | get_icon = SelectWorkdir = GUIColors = MoveToDialog = None 15 | try: 16 | import wx 17 | from .wxutils import get_icon, SelectWorkdir, GUIColors 18 | from .moveto_dialog import MoveToDialog 19 | HAS_WXPYTHON = True 20 | except ImportError: 21 | pass 22 | -------------------------------------------------------------------------------- /epicsapps/utils/configfile.py: -------------------------------------------------------------------------------- 1 | from os import environ as ENV 2 | from pathlib import Path 3 | import yaml 4 | 5 | # note: toml.loads cannot load strings that toml.dumps writes, 6 | # especialy with complex data structures. 7 | # For Py3.11+, we use toml.dumps() and tomllib.loads(). 8 | # For Py<3.11, we use toml.dumps() and tomli.loads(). 9 | import toml 10 | try: 11 | import tomllib 12 | except ImportError: 13 | import tomli as tomllib 14 | 15 | from pyshortcuts.utils import get_homedir 16 | 17 | 18 | def load_yaml(text): 19 | """very simple yaml loader""" 20 | return yaml.load(text, Loader=yaml.Loader) 21 | 22 | def load_toml(text): 23 | """very simple toml loader""" 24 | return tomllib.loads(text) 25 | 26 | def get_configfolder(): 27 | """ 28 | get an epicsapps config folder 29 | Returns: 30 | path name of config folder, typically $HOME/.config/epicsapps 31 | 32 | """ 33 | escancred = ENV.get('ESCAN_CREDENTIALS', None) 34 | if escancred is not None: 35 | confdir = Path(escancred).parent 36 | else: 37 | confdir = Path(get_homedir(), '.config', 'epicsapps') 38 | 39 | if not confdir.exists(): 40 | try: 41 | confdir.mkdir(mode=0o755, parents=True, exist_ok=True) 42 | except FileExistsError: 43 | pass 44 | return confdir.as_posix() 45 | 46 | def get_default_configfile(fname): 47 | """get the default configfile, if it exists or None if it does not""" 48 | path = Path(get_configfolder(), fname) 49 | if path.exists(): 50 | return path.as_posix() 51 | return None 52 | 53 | def read_recents_file(fname='recent_ad_pvs.txt'): 54 | fpath = Path(get_configfolder(), fname) 55 | lines = ['#'] 56 | out = [] 57 | if fpath.exists(): 58 | with open(fpath, 'r') as fh: 59 | lines = fh.readlines() 60 | for line in lines: 61 | if not line.startswith('#') and len(line) > 2: 62 | out.append(line[:-1]) 63 | return out 64 | 65 | def write_recents_file(fname='recent_ad_pvs.txt', nlist=None): 66 | if nlist is not None and len(nlist) > 0: 67 | fpath = Path(get_configfolder(), fname) 68 | with open(fpath, 'w') as fh: 69 | fh.write('\n'.join(nlist)) 70 | 71 | class ConfigFile(object): 72 | """ 73 | Configuration File, using either YAML or TOML 74 | The ConfigFile will have attributes / methods: 75 | 76 | config: dict of configuration data 77 | default_config: factory default configuration data 78 | filename: name of config file 79 | reset_default(): set config to factory default 80 | read(fname): read config from file 81 | write(fname=None, config=None): write config to file 82 | 83 | """ 84 | def __init__(self, fname=None, default_config=None): 85 | self.filename = fname 86 | self.default_config = {} 87 | if default_config is not None: 88 | self.default_config.update(default_config) 89 | 90 | self.config = {} 91 | self.config.update(self.default_config) 92 | if fname is not None: 93 | self.read(fname) 94 | 95 | def reset_default(self): 96 | """reset config to initial / factory default""" 97 | self.config = {} 98 | self.config.update(self.default_config) 99 | 100 | def read(self, fname): 101 | """read config file 102 | 103 | Arguments: 104 | fname (str): name of configuration file 105 | 106 | Notes: 107 | 1. if the file is found in the current working folder or because the 108 | full path is given, that file will be read. 109 | 2. if the file is not found, but a file with that name is found in 110 | the default configuration folder ($HOME/.config/epicsapps), that 111 | file will be read. 112 | 113 | """ 114 | fpath = Path(fname).absolute() 115 | if not fpath.exists(): 116 | _path = Path(get_configfolder(), fname) 117 | if _path.exists(): 118 | fpath = _path 119 | 120 | if not fpath.exists(): 121 | print("No config file to read: ", fname) 122 | self.config = self.default_config 123 | return 124 | 125 | 126 | self.filename = fpath.absolute().as_posix() 127 | stem = fpath.suffix 128 | text = None 129 | with open(self.filename, 'r') as fh: 130 | text = fh.read() 131 | 132 | formats = ['yaml', 'toml'] 133 | if fpath.suffix.endswith('toml'): 134 | formats = ['toml', 'yaml'] 135 | for form in formats: 136 | if form == 'toml': 137 | try: 138 | ret = tomllib.loads(text) 139 | if isinstance(ret, dict): 140 | self.config = ret 141 | except: 142 | print("Warning could not read TOML Config") 143 | elif form == 'yaml': 144 | try: 145 | ret = yaml.load(text, Loader=yaml.Loader) 146 | if isinstance(ret, dict): 147 | self.config = ret 148 | except: 149 | pass 150 | 151 | if self.config is not None: 152 | break 153 | 154 | def write(self, fname=None, config=None): 155 | if fname is None: 156 | fname = self.filename 157 | 158 | if config is None: 159 | config = self.config 160 | 161 | fpath = Path(fname) 162 | if fpath.suffix == '.yaml': 163 | with open(fpath, 'w') as fh: 164 | yaml.dump(config, fh, default_flow_style=None) 165 | else: 166 | with open(fpath, 'w') as fh: 167 | fh.write(toml.dumps(config)) 168 | -------------------------------------------------------------------------------- /epicsapps/utils/debugtimer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import time 4 | import sys 5 | from tabulate import tabulate 6 | 7 | class DebugTimer(): 8 | ''' 9 | Measure run times for lines of code and summarize results 10 | ''' 11 | def __init__(self, initial_message=None, verbose=False, 12 | with_mod_count=False, precision=3): 13 | self.verbose = verbose 14 | self.precision = precision 15 | self.with_mod_count = with_mod_count 16 | self.clear(initial_message=initial_message) 17 | 18 | def clear(self, initial_message=None): 19 | self.data = [] 20 | if initial_message is None: 21 | initial_message = 'DebugTimer' 22 | self.add(f'initial_message {time.ctime()}') 23 | 24 | def add(self, msg=None): 25 | if msg is None: 26 | msg = time.ctime() 27 | self.data.append((msg, len(sys.modules), time.perf_counter())) 28 | if self.verbose: 29 | print(msg) 30 | 31 | def get_table(self, precision=None, with_mod_count=True, 32 | tablefmt='simple_outline'): 33 | prec = self.precision 34 | if precision is not None: 35 | prec = precision 36 | with_nmod= self.with_mod_count 37 | if with_mod_count is not None: 38 | with_nmod = with_mod_count 39 | m0, n0, t0 = self.data[0] 40 | tprev= t0 41 | header = ['Message','Delta Time', 'Total Time'] 42 | row = [m0, 0, 0] 43 | if with_nmod: 44 | header.append('# Modules') 45 | row.append(n0) 46 | table = [row[:]] 47 | for m, n, t in self.data[1:]: 48 | tt, dt = t-t0, t-tprev 49 | row = [m, dt, tt, n] if with_nmod else [m, dt, tt] 50 | table.append(row[:]) 51 | tprev = t 52 | ffmt = f'.{prec}f' 53 | return tabulate(table, header, floatfmt=ffmt, tablefmt=tablefmt) 54 | 55 | def show(self, precision=None, with_mod_count=True, 56 | tablefmt='outline'): 57 | print(self.get_table(precision=precision, 58 | with_mod_count=with_mod_count, 59 | tablefmt=tablefmt)) 60 | 61 | 62 | def debugtimer(initial_message=None, with_mod_count=False, 63 | verbose=False, precision=3): 64 | '''debugtimer returns a DebugTimer object to measure the runtime of 65 | portions of code, and then write a simple report of the results. 66 | 67 | Arguments 68 | ------------ 69 | iniitial message: str, optional initial message ['DebugTimer'] 70 | precision: int, precision for timing results [3] 71 | with_mod_count: bool, whether to include number of imported modules [True] 72 | verbose: bool, whether to print() each message when entered [False] 73 | 74 | Returns 75 | -------- 76 | DebugTimer object, with methods: 77 | 78 | clear(initial_message=None) -- reset Timer 79 | add(message) -- record time, with message 80 | show(tablefmt='simple_outline') -- print timer report 81 | where tableftmt can be any tablefmt for the tabulate module. 82 | 83 | Example: 84 | ------- 85 | timer = debugtimer('started timer', precision=4) 86 | result = foo(x=100) 87 | timer.add('ran foo') 88 | bar(result) 89 | timer.add('ran bar') 90 | timer.show(tablefmt='outline') 91 | ''' 92 | return DebugTimer(initial_message=initial_message, 93 | with_mod_count=with_mod_count, 94 | verbose=verbose, precision=precision) 95 | 96 | 97 | if __name__ == '__main__': 98 | dt = debugtimer('test timer') 99 | time.sleep(1.102) 100 | dt.add('slept for 1.102 seconds') 101 | import numpy as np 102 | n = 10_000_000 103 | x = np.arange(n, dtype='float64')/3.0 104 | dt.add(f'created numpy array len={n}') 105 | s = np.sqrt(x) 106 | dt.add('took sqrt') 107 | dt.show(tablefmt='outline') 108 | -------------------------------------------------------------------------------- /epicsapps/utils/moveto_dialog.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | import time 3 | import wx 4 | from wxutils import pack, Popup, Button, SimpleText 5 | 6 | from . import GUIColors 7 | 8 | ALL_EXP = wx.ALL|wx.EXPAND 9 | 10 | class MoveToDialog(wx.Frame): 11 | """Full Query for Move To for a Position""" 12 | msg = '''Select Recent Instrument File, create a new one''' 13 | def __init__(self, parent, pvdata, instname, posname, callback=None, mode=None): 14 | self.pvdata = pvdata 15 | self.callback = callback 16 | 17 | title = "Move Instrument %s to Position '%s'?" % (instname, posname) 18 | if mode == 'show': 19 | title = "Instrument %s / Position '%s'" % (instname, posname) 20 | wx.Frame.__init__(self, parent, wx.ID_ANY, title=title, 21 | size=(500, 325)) 22 | 23 | colors = GUIColors() 24 | self.SetBackgroundColour(parent.GetBackgroundColour()) 25 | self.SetFont(parent.GetFont()) 26 | titlefont = self.GetFont() 27 | titlefont.PointSize += 2 28 | titlefont.SetWeight(wx.BOLD) 29 | 30 | sizer = wx.GridBagSizer(10, 4) 31 | 32 | labstyle = wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.ALL 33 | rlabstyle = wx.ALIGN_RIGHT|wx.ALIGN_CENTER_VERTICAL|wx.ALL 34 | tstyle = wx.ALIGN_RIGHT|wx.ALIGN_CENTER_VERTICAL 35 | 36 | # title row 37 | i = 0 38 | col_labels = [' PV ', 'Current Value', 'Saved Value'] 39 | if mode != 'show': 40 | col_labels.append('Move?') 41 | 42 | for titleword in col_labels: 43 | txt =SimpleText(self, titleword, font=titlefont, size=(125,-1), 44 | colour=colors.title, style=tstyle) 45 | sizer.Add(txt, (0, i), (1, 1), labstyle, 1) 46 | i = i + 1 47 | 48 | sizer.Add(wx.StaticLine(self, size=(450, -1), 49 | style=wx.LI_HORIZONTAL), 50 | (1, 0), (1, 4), wx.ALIGN_CENTER|wx.GROW|wx.ALL, 0) 51 | 52 | self.checkboxes = {} 53 | irow = 1 54 | for pvname, data in pvdata.items(): 55 | irow += 1 56 | desc, save_val, curr_val = data 57 | label = SimpleText(self, desc, style=tstyle) 58 | curr = SimpleText(self, curr_val, style=tstyle) 59 | saved = SimpleText(self, save_val, style=tstyle) 60 | if mode != 'show': 61 | cbox = wx.CheckBox(self, -1, "Move") 62 | cbox.SetValue(True) 63 | self.checkboxes[pvname] = cbox 64 | sizer.Add(label, (irow, 0), (1, 1), labstyle, 2) 65 | sizer.Add(curr, (irow, 1), (1, 1), rlabstyle, 2) 66 | sizer.Add(saved, (irow, 2), (1, 1), rlabstyle, 2) 67 | if mode != 'show': 68 | sizer.Add(cbox, (irow, 3), (1, 1), rlabstyle, 2) 69 | irow += 1 70 | sizer.Add(wx.StaticLine(self, size=(450, -1), 71 | style=wx.LI_HORIZONTAL), 72 | (irow, 0), (1, 4), wx.ALIGN_CENTER|wx.GROW|wx.ALL, 0) 73 | 74 | irow += 1 75 | self.ok_selected = False 76 | 77 | btnsizer = wx.StdDialogButtonSizer() 78 | btn_ok = wx.Button(self, wx.ID_OK) 79 | btn_no = wx.Button(self, wx.ID_CANCEL) 80 | self.Bind(wx.EVT_BUTTON, self.onOK, btn_ok) 81 | self.Bind(wx.EVT_BUTTON, self.onCancel, btn_no) 82 | btnsizer.AddButton(btn_ok) 83 | btnsizer.AddButton(btn_no) 84 | btnsizer.Realize() 85 | 86 | sizer.Add(btnsizer, (irow, 1), (1, 3), 87 | wx.ALIGN_CENTER_VERTICAL|wx.ALL, 1) 88 | pack(self, sizer) 89 | w, h = self.GetBestSize() 90 | w = 25*int((w + 26)/25.) 91 | h = 25*int((h + 26)/25.) 92 | self.SetSize((w, h)) 93 | self.Show() 94 | 95 | def onOK(self, event=None): 96 | values = {} 97 | for pvname in self.pvdata: 98 | if pvname in self.checkboxes: 99 | if self.checkboxes[pvname].IsChecked(): 100 | values[pvname] = self.pvdata[pvname][1] 101 | if callable(self.callback): 102 | self.callback(values) 103 | self.Destroy() 104 | 105 | def onCancel(self, event=None): 106 | self.Destroy() 107 | -------------------------------------------------------------------------------- /epicsapps/utils/textfile.py: -------------------------------------------------------------------------------- 1 | """ 2 | text file utilities 3 | """ 4 | import io 5 | from pathlib import Path 6 | from charset_normalizer import from_bytes 7 | 8 | def read_textfile(filename, size=None): 9 | """read text from a file as string 10 | 11 | Argument 12 | -------- 13 | filename (str or file): name of file to read or file-like object 14 | size (int or None): number of bytes to read 15 | 16 | Returns 17 | ------- 18 | text of file as string. 19 | 20 | Notes 21 | ------ 22 | 1. the encoding is detected with charset_normalizer.from_bytes 23 | which is then used to decode bytes read from file. 24 | 2. line endings are normalized to be '\n', so that 25 | splitting on '\n' will give a list of lines. 26 | 3. if filename is given, it can be a gzip-compressed file 27 | """ 28 | text = '' 29 | if isinstance(filename, bytes): 30 | filename = str(from_bytes(filename).best()) 31 | if isinstance(filename, str): 32 | filename = Path(filename).absolute() 33 | if isinstance(filename, Path): 34 | filename = filename.as_posix() 35 | 36 | 37 | def decode(bytedata): 38 | return str(from_bytes(bytedata).best()) 39 | 40 | if isinstance(filename, io.IOBase): 41 | text = filename.read(size) 42 | if filename.mode == 'rb': 43 | text = deco 1: 35 | typename = 'string' 36 | 37 | elif inst_pv is not None and isinstance(pvobj, inst_pv): 38 | typename = str(pvobj.pvtype.name) 39 | 40 | # now we have typename: use as default, add alternate choices 41 | if typename == 'motor': 42 | choices = ['motor', 'numeric', 'string'] 43 | elif typename in ('enum', 'time_enum'): 44 | choices = ['enum', 'numeric', 'string'] 45 | elif typename in ('string', 'time_string'): 46 | choices = ['string', 'numeric'] 47 | return tuple(choices) 48 | 49 | def get_pvdesc(pvname): 50 | desc = pref = pvname 51 | if '.' in pvname: 52 | pref = pvname[:pvname.find('.')] 53 | descpv = epics.get_pv(pref + '.DESC', form='native') 54 | if descpv.connect(): 55 | desc = descpv.get() 56 | return desc 57 | -------------------------------------------------------------------------------- /epicsapps/utils/wxutils.py: -------------------------------------------------------------------------------- 1 | """ 2 | wxPython utils 3 | """ 4 | import os 5 | import wx 6 | 7 | def get_icon(iconname): 8 | topdir, _s = os.path.split(__file__) 9 | topdir, _s = os.path.split(topdir) 10 | if not iconname.endswith('.ico'): 11 | iconname = "%s.ico" % iconname 12 | return os.path.abspath(os.path.join(topdir, 'icons', iconname)) 13 | 14 | def SelectWorkdir(parent, message='Select Working Folder...'): 15 | "prompt for and change into a working directory " 16 | dlg = wx.DirDialog(parent, message, 17 | style=wx.DD_DEFAULT_STYLE|wx.DD_CHANGE_DIR) 18 | 19 | path = os.path.abspath(os.curdir) 20 | dlg.SetPath(path) 21 | if dlg.ShowModal() == wx.ID_CANCEL: 22 | return None 23 | path = os.path.abspath(dlg.GetPath()) 24 | dlg.Destroy() 25 | os.chdir(path) 26 | return path 27 | 28 | class GUIColors(object): 29 | def __init__(self): 30 | self.bg = wx.Colour(250,250,240) 31 | self.nb_active = wx.Colour(254,254,195) 32 | self.nb_area = wx.Colour(250,250,245) 33 | self.nb_text = wx.Colour(10,10,180) 34 | self.nb_activetext = wx.Colour(80,10,10) 35 | self.title = wx.Colour(80,10,10) 36 | self.pvname = wx.Colour(10,10,80) 37 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | This folder contains example configuration files for 2 | 3 | AreaDetector Viewer: 4 | three example yaml files are included. 5 | 6 | Instruments: 7 | an empty sqlite database is included. 8 | 9 | Microscope: 10 | one example yaml file is included. 11 | 12 | ## Using Epics_SCAN 13 | 14 | Some of these applications make use of an ESCAN postgresql database, 15 | as also used by the EpicsScan data collection software. 16 | 17 | By default, the environmental variable ESCAN_CREDENTIALS is used to point to 18 | the (yaml formatted) file that defines how to connect to the ESCAN postgresql 19 | database. 20 | 21 | epics_scan/escan_credentials.dat 22 | epics_scan/escan_pgschema.sql 23 | -------------------------------------------------------------------------------- /examples/areaDetector/ID13_Eiger500K.yaml: -------------------------------------------------------------------------------- 1 | camera_attributes: [Acquire, DetectorState_RBV, ArrayCounter, ArrayCounter_RBV, NumImages, 2 | NumImages_RBV, AcquireTime, AcquireTime_RBV, TriggerMode, TriggerMode_RBV] 3 | colormaps: [gray, magma, inferno, plasma, viridis, coolwarm, hot, jet] 4 | colormode: Mono 5 | default_rotation: 0 6 | enabled_plugins: [image1, Over1, Over2, Over3, Over4, ROI1, ROI2, JPEG1, TIFF1] 7 | epics_controls: 8 | - [Trigger Mode, 'cam1:TriggerMode', true, pvenum, _RBV, 150, 10] 9 | - ['# Images', 'cam1:NumImages', true, pvfloat, _RBV, 100, 10] 10 | - [Acqure Period, 'cam1:AcquirePeriod', true, pvfloat, _RBV, 100, 10] 11 | - [Acquire Time, 'cam1:AcquireTime', true, pvfloat, _RBV, 100, 10] 12 | - [X-ray Energy, 'cam1:PhotonEnergy', true, pvfloat, _RBV, 100, 10] 13 | - [Energy Threshold, 'cam1:ThresholdEnergy', true, pvfloat, _RBV, 100, 10] 14 | - [TIFF File Path, 'TIFF1:FilePath', true, pvtctrl, false, 250, 10] 15 | - [Acquire Status, 'cam1:Acquire', true, pvtext, false, 250, 10] 16 | - [Acquire Busy, 'cam1:AcquireBusy', true, pvtext, false, 250, 10] 17 | - [Acquire Message, 'cam1:StatusMessage_RBV', true, pvtext, false, 250, 10] 18 | - [Detector Armed, 'cam1:Armed', true, pvtext, false, 250, 10] 19 | - [Free Disk Space (Gb), 'cam1:FWFree_RBV', true, pvtext, false, 250, 10] 20 | filesaver: 'TIFF1:' 21 | free_run_time: 0.2 22 | iconfile: eiger500k.ico 23 | image_attributes: [ArrayData, UniqueId_RBV] 24 | int1d_flipx': false 25 | int1d_flipy': true 26 | int1d_trimx': 0 27 | int1d_trimy': 0 28 | name: Eiger 29 | prefix: '13EIG1:' 30 | scandb_instrument: Eiger Stages 31 | show_1dintegration: true 32 | show_free_run: false 33 | show_thumbnail: true 34 | thumbnail_size: 100 35 | title: Epics areaDetector Display 36 | use_filesaver: true 37 | workdir: /home/xas_user/Codes/epicsapps 38 | -------------------------------------------------------------------------------- /examples/areaDetector/ID13_SecondarySourceAperture.yaml: -------------------------------------------------------------------------------- 1 | camera_attributes: [Acquire, ArrayCounter, ArrayCounter_RBV, NumImages, NumImages_RBV, 2 | AcquireTime, AcquireTime_RBV, TriggerMode, TriggerMode_RBV] 3 | colormaps: [gray, magma, inferno, plasma, viridis, coolwarm, hot, jet] 4 | colormode: Mono 5 | default_rotation: 0 6 | enabled_plugins: [image1, Over1, Over2, Over3, Over4, ROI1, ROI2, JPEG1, TIFF1] 7 | epics_controls: 8 | - [Trigger Mode, 'cam1:TriggerMode', true, pvenum, _RBV, 150, 10] 9 | - [Image Mode, 'cam1:ImageMode', true, pvenum, _RBV, 150, 10] 10 | - ['# Images', 'cam1:NumImages', true, pvfloat, _RBV, 100, 10] 11 | - [Acquire Time, 'cam1:AcquireTime', true, pvfloat, _RBV, 100, 10] 12 | - [Acquire Period, 'cam1:AcquirePeriod', true, pvfloat, _RBV, 100, 10] 13 | - [TIFF File Path, 'TIFF1:FilePath', true, pvtctrl, false, 250, 10] 14 | - [Acquire Status, 'cam1:Acquire', true, pvtext, false, 250, 10] 15 | filesaver: 'TIFF1:' 16 | free_run_time: 0.2 17 | iconfile: None 18 | image_attributes: [ArrayData, UniqueId_RBV] 19 | int1d_flipx': false 20 | int1d_flipy': true 21 | int1d_trimx': 0 22 | int1d_trimy': 0 23 | name: Secondary Source Aperture Viewer 24 | prefix: '13IDBPS1:' 25 | scandb_instrument: SSA Viewscreen 26 | show_1dintegration: false 27 | show_free_run: false 28 | show_thumbnail: true 29 | thumbnail_size: 100 30 | title: AD Display / IDB SSA Viewer 31 | use_filesaver: true 32 | workdir: /home/xas_user/Codes/epicsapps 33 | -------------------------------------------------------------------------------- /examples/areaDetector/ID13_foe_camera.yaml: -------------------------------------------------------------------------------- 1 | camera_attributes: [Acquire, ArrayCounter, ArrayCounter_RBV, NumImages, NumImages_RBV, 2 | AcquireTime, AcquireTime_RBV, TriggerMode, TriggerMode_RBV] 3 | colormaps: [gray, magma, inferno, plasma, viridis, coolwarm, hot, jet] 4 | colormode: Mono 5 | default_rotation: 0 6 | enabled_plugins: [image1, Over1, Over2, Over3, Over4, ROI1, ROI2, JPEG1, TIFF1] 7 | epics_controls: 8 | - [Trigger Mode, 'cam1:TriggerMode', true, pvenum, _RBV, 150, 10] 9 | - [Image Mode, 'cam1:ImageMode', true, pvenum, _RBV, 150, 10] 10 | - ['# Images', 'cam1:NumImages', true, pvfloat, _RBV, 100, 10] 11 | - [Acquire Time, 'cam1:AcquireTime', true, pvfloat, _RBV, 100, 10] 12 | - [Acquire Period, 'cam1:AcquirePeriod', true, pvfloat, _RBV, 100, 10] 13 | - [TIFF File Path, 'TIFF1:FilePath', true, pvtctrl, false, 250, 10] 14 | - [Acquire Status, 'cam1:Acquire', true, pvtext, false, 250, 10] 15 | filesaver: 'TIFF1:' 16 | free_run_time: 0.2 17 | iconfile: None 18 | image_attributes: [ArrayData, UniqueId_RBV] 19 | int1d_flipx': false 20 | int1d_flipy': true 21 | int1d_trimx': 0 22 | int1d_trimy': 0 23 | name: IDA Beam Viewer 24 | prefix: '13IDAPG1:' 25 | scandb_instrument: Pinhole Tank BPM 26 | show_1dintegration: false 27 | show_free_run: false 28 | show_thumbnail: true 29 | thumbnail_size: 100 30 | title: AD Display / IDA Beam Viewer 31 | use_filesaver: true 32 | workdir: /home/xas_user/Codes/epicsapps 33 | -------------------------------------------------------------------------------- /examples/areaDetector/eiger500k.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyepics/epicsapps/90cff8b8f6f6ed5c001f52ff47f06fc0ae1b5e82/examples/areaDetector/eiger500k.ico -------------------------------------------------------------------------------- /examples/epics_scan/escan_credentials.dat: -------------------------------------------------------------------------------- 1 | server: postgresql 2 | host: pghost.sector.aps.anl.gov 3 | user: pguser 4 | dbname: escan_SectorStation 5 | password: secret_word 6 | port: 5432 7 | -------------------------------------------------------------------------------- /examples/instruments/epics_client/PyInstrument.adl: -------------------------------------------------------------------------------- 1 | 2 | file { 3 | name="/home/epics/support/CARS/CARSApp/op/adl/PyInstrument.adl" 4 | version=030102 5 | } 6 | display { 7 | object { 8 | x=654 9 | y=28 10 | width=400 11 | height=220 12 | } 13 | clr=14 14 | bclr=2 15 | cmap="" 16 | gridSpacing=5 17 | gridOn=1 18 | snapToGrid=1 19 | } 20 | "color map" { 21 | ncolors=65 22 | colors { 23 | ffffff, 24 | ececec, 25 | dadada, 26 | c8c8c8, 27 | bbbbbb, 28 | aeaeae, 29 | 9e9e9e, 30 | 919191, 31 | 858585, 32 | 787878, 33 | 696969, 34 | 5a5a5a, 35 | 464646, 36 | 2d2d2d, 37 | 000000, 38 | 00d800, 39 | 1ebb00, 40 | 339900, 41 | 2d7f00, 42 | 216c00, 43 | fd0000, 44 | de1309, 45 | be190b, 46 | a01207, 47 | 820400, 48 | 5893ff, 49 | 597ee1, 50 | 4b6ec7, 51 | 3a5eab, 52 | 27548d, 53 | fbf34a, 54 | f9da3c, 55 | eeb62b, 56 | e19015, 57 | cd6100, 58 | ffb0ff, 59 | d67fe2, 60 | ae4ebc, 61 | 8b1a96, 62 | 610a75, 63 | a4aaff, 64 | 8793e2, 65 | 6a73c1, 66 | 4d52a4, 67 | 343386, 68 | c7bb6d, 69 | b79d5c, 70 | a47e3c, 71 | 7d5627, 72 | 58340f, 73 | 99ffff, 74 | 73dfff, 75 | 4ea5f9, 76 | 2a63e4, 77 | 0a00b8, 78 | ebf1b5, 79 | d4db9d, 80 | bbc187, 81 | a6a462, 82 | 8b8239, 83 | 73ff6b, 84 | 52da3b, 85 | 3cb420, 86 | 289315, 87 | 1a7309, 88 | } 89 | } 90 | oval { 91 | object { 92 | x=360 93 | y=115 94 | width=15 95 | height=15 96 | } 97 | "basic attribute" { 98 | clr=20 99 | } 100 | "dynamic attribute" { 101 | vis="if zero" 102 | chan="$(P)$(Q):PosOK" 103 | } 104 | } 105 | oval { 106 | object { 107 | x=360 108 | y=80 109 | width=15 110 | height=15 111 | } 112 | "basic attribute" { 113 | clr=20 114 | } 115 | "dynamic attribute" { 116 | vis="if zero" 117 | chan="$(P)$(Q):InstOK" 118 | } 119 | } 120 | text { 121 | object { 122 | x=8 123 | y=8 124 | width=140 125 | height=20 126 | } 127 | "basic attribute" { 128 | clr=54 129 | } 130 | textix="Instruments" 131 | } 132 | "text entry" { 133 | object { 134 | x=103 135 | y=112 136 | width=240 137 | height=25 138 | } 139 | control { 140 | chan="$(P)$(Q):PosName" 141 | clr=14 142 | bclr=0 143 | } 144 | format="string" 145 | limits { 146 | } 147 | } 148 | text { 149 | object { 150 | x=13 151 | y=112 152 | width=50 153 | height=18 154 | } 155 | "basic attribute" { 156 | clr=14 157 | } 158 | textix="Position:" 159 | } 160 | "message button" { 161 | object { 162 | x=10 163 | y=150 164 | width=75 165 | height=25 166 | } 167 | control { 168 | chan="$(P)$(Q):Move" 169 | clr=14 170 | bclr=55 171 | } 172 | label="Move" 173 | press_msg="1" 174 | } 175 | "text update" { 176 | object { 177 | x=5 178 | y=35 179 | width=400 180 | height=18 181 | } 182 | monitor { 183 | chan="$(P)$(Q):Message" 184 | clr=24 185 | bclr=1 186 | } 187 | format="string" 188 | limits { 189 | } 190 | } 191 | "text update" { 192 | object { 193 | x=195 194 | y=10 195 | width=200 196 | height=18 197 | } 198 | monitor { 199 | chan="$(P)$(Q):TSTAMP" 200 | clr=24 201 | bclr=1 202 | } 203 | format="string" 204 | limits { 205 | } 206 | } 207 | "text update" { 208 | object { 209 | x=5 210 | y=195 211 | width=410 212 | height=14 213 | } 214 | monitor { 215 | chan="$(P)$(Q):Info" 216 | clr=24 217 | bclr=2 218 | } 219 | format="string" 220 | limits { 221 | } 222 | } 223 | text { 224 | object { 225 | x=13 226 | y=77 227 | width=50 228 | height=18 229 | } 230 | "basic attribute" { 231 | clr=14 232 | } 233 | textix="Instrument:" 234 | } 235 | "text entry" { 236 | object { 237 | x=103 238 | y=73 239 | width=240 240 | height=25 241 | } 242 | control { 243 | chan="$(P)$(Q):InstName" 244 | clr=14 245 | bclr=0 246 | } 247 | format="string" 248 | limits { 249 | } 250 | } 251 | oval { 252 | object { 253 | x=360 254 | y=80 255 | width=15 256 | height=15 257 | } 258 | "basic attribute" { 259 | clr=15 260 | } 261 | "dynamic attribute" { 262 | vis="if not zero" 263 | chan="$(P)$(Q):InstOK" 264 | } 265 | } 266 | oval { 267 | object { 268 | x=360 269 | y=115 270 | width=15 271 | height=15 272 | } 273 | "basic attribute" { 274 | clr=15 275 | } 276 | "dynamic attribute" { 277 | vis="if not zero" 278 | chan="$(P)$(Q):PosOK" 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /examples/instruments/epics_client/PyInstrument.db: -------------------------------------------------------------------------------- 1 | # This database contains fields to allow communication between 2 | # the Epics Instrument GUI and other Channel Access clients 3 | 4 | record(stringin,"$(P)$(Q):TSTAMP") { 5 | field(DTYP,"Soft Channel") 6 | field(DESC, "timestamp") 7 | field(VAL, "Starting") 8 | } 9 | 10 | record(longout,"$(P)$(Q):UNIXTS") { 11 | field(DTYP,"Soft Channel") 12 | field(DESC, "timestamp") 13 | field(VAL, 0) 14 | } 15 | 16 | record(mbbo,"$(P)$(Q):ExecCommand") { 17 | field(DESC,"Exec Command") 18 | field(ZRVL,"0") 19 | field(ZRST,"Stop") 20 | field(ONVL,"1") 21 | field(ONST,"Go") 22 | } 23 | 24 | record(busy,"$(P)$(Q):Move") { 25 | field(DESC,"Client Move Request") 26 | } 27 | 28 | record(stringin,"$(P)$(Q):InstName") { 29 | field(DTYP,"Soft Channel") 30 | field(DESC, "instrument name") 31 | field(VAL, "") 32 | } 33 | 34 | record(stringin,"$(P)$(Q):PosName") { 35 | field(DTYP,"Soft Channel") 36 | field(DESC, "position name") 37 | field(VAL, "") 38 | } 39 | 40 | 41 | record(bo,"$(P)$(Q):InstOK") { 42 | field(VAL, "0") 43 | } 44 | 45 | 46 | record(bo,"$(P)$(Q):PosOK") { 47 | field(VAL, "0") 48 | } 49 | 50 | record(stringin,"$(P)$(Q):CommandName") { 51 | field(DTYP,"Soft Channel") 52 | field(DESC, "command name") 53 | field(VAL, "") 54 | } 55 | 56 | 57 | record(bo,"$(P)$(Q):CommandOK") { 58 | field(DESC,"Command found") 59 | field(VAL, "0") 60 | } 61 | 62 | record(stringin,"$(P)$(Q):CommandArg1") { 63 | field(DTYP,"Soft Channel") 64 | field(DESC,"command arg 1") 65 | field(VAL, "") 66 | } 67 | 68 | record(stringin,"$(P)$(Q):CommandArg2") { 69 | field(DTYP,"Soft Channel") 70 | field(DESC,"command arg 2") 71 | field(VAL, "") 72 | } 73 | 74 | record(stringout,"$(P)$(Q):Message") { 75 | field(DTYP,"Soft Channel") 76 | field(DESC,"output message") 77 | field(VAL, "") 78 | } 79 | 80 | record(waveform,"$(P)$(Q):Info") { 81 | field(DTYP,"Soft Channel") 82 | field(DESC,"info string") 83 | field(NELM,"128") 84 | field(FTVL,"CHAR") 85 | } 86 | 87 | -------------------------------------------------------------------------------- /examples/instruments/epics_client/epics_inst.pro: -------------------------------------------------------------------------------- 1 | function epics_inst, prefix 2 | return, obj_new('epics_instrument', prefix=prefix) 3 | end 4 | -------------------------------------------------------------------------------- /examples/instruments/epics_client/epics_instrument__define.pro: -------------------------------------------------------------------------------- 1 | ; 2 | ; Epics Instrument 3 | 4 | function epics_instrument::move_to_position, position, instrument=instrument, wait=wait 5 | 6 | ;+ 7 | ; NAME: instrument::move_to_position 8 | ; 9 | ; PURPOSE: move Instrument to named Position 10 | ; 11 | ; CALLING SEQUENCE: inst->move_to_position(position, /wait) 12 | 13 | ; INPUTS: position - position name 14 | ; 15 | ; KEYWORD PARAMETERS: instrument - name of instrument 16 | ; wait - flag to wait for completion of move 17 | ; 18 | ; OUTPUTS: returns -1 on error (position invalid, no instrument) 19 | ; returns 0 on success 20 | ; 21 | ; EXAMPLE: inst = obj_new('epics_instrument', prefix='13XRM:Inst') 22 | ; print, inst->move_to_position, 'Spot1', 'Sample Stage', /wait 23 | ; 24 | ; MODIFICATION HISTORY: 2011-Apr-29 M Newville 25 | ; 26 | ;- 27 | if (keyword_set(instrument) ne 0) then begin 28 | ret = self->set_instrument(instrument) 29 | if (ret ne 1) then return, -1 30 | endif 31 | 32 | ret = self->set_position(position) 33 | if (ret ne 1) then return, -1 34 | 35 | x = caput(self.prefix + 'Move', 1) 36 | if (keyword_set(wait) ne 0) then begin 37 | moving = 1 38 | while (moving eq 1) do begin 39 | wait, 0.1 40 | x = caget(self.prefix + 'Move', moving) 41 | endwhile 42 | endif 43 | return, 0 44 | end 45 | 46 | 47 | function epics_instrument::set_position, position 48 | 49 | self.position = position 50 | x = caput(self.prefix + 'PosName', position) 51 | wait, 0.5 52 | x = caget(self.prefix + 'PosOK', pos_ok) 53 | return, pos_ok 54 | end 55 | 56 | 57 | function epics_instrument::set_instrument, inst_name 58 | 59 | self.instrument = inst_name 60 | x = caput(self.prefix + 'InstName', inst_name) 61 | wait, 0.5 62 | x = caget(self.prefix + 'InstOK', inst_ok) 63 | return, inst_ok 64 | end 65 | 66 | function epics_instrument::init, prefix=prefix 67 | self.prefix = prefix 68 | return, 1 69 | end 70 | 71 | pro epics_instrument__define 72 | ;+ 73 | ; NAME: 74 | ; EPICS_INSTRUMENT__DEFINE 75 | ; 76 | ; PURPOSE: 77 | ; Defines an Epics Instrument object that interacts with Epics Record of PyInstrumet 78 | ; 79 | ; 80 | ; CALLING SEQUENCE: 81 | ; einst = obj_new('epics_instrument', '13XRM:Inst:') 82 | ; 83 | ; 84 | ; INPUTS: 85 | ; None. 86 | ; 87 | ; OPTIONAL INPUTS: 88 | ; None. 89 | ; 90 | ; KEYWORD PARAMETERS: 91 | ; None. 92 | ; 93 | ; 94 | ; OUTPUTS: 95 | ; Return value will contain the object reference. 96 | ; 97 | ; 98 | ; OPTIONAL OUTPUTS: 99 | ; None. 100 | ; 101 | ; 102 | ; COMMON BLOCKS: 103 | ; None. 104 | ; 105 | ; 106 | ; SIDE EFFECTS: 107 | ; EPICS_Instrument object is created. 108 | ; 109 | ; 110 | ; EXAMPLE: 111 | ; einst = obj_new('epics_instrument', '13XRM:Inst') 112 | ; einst->move_to_position, 'Spot1', 'Sample Stage', /wait 113 | ; 114 | ; MODIFICATION HISTORY: 2011-Apr-29 M Newville 115 | ;- 116 | 117 | epics_instrument = {epics_instrument, prefix: '', instrument: '', position: ''} 118 | end 119 | -------------------------------------------------------------------------------- /examples/instruments/example_sqlite.ein: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyepics/epicsapps/90cff8b8f6f6ed5c001f52ff47f06fc0ae1b5e82/examples/instruments/example_sqlite.ein -------------------------------------------------------------------------------- /examples/instruments/instruments.yaml: -------------------------------------------------------------------------------- 1 | server: postgresql 2 | dbname: escan_aps13ide 3 | host: cars4.cars.aps.anl.gov 4 | user: username 5 | password: secret 6 | port: '5432' 7 | recent_dbs: [] 8 | -------------------------------------------------------------------------------- /examples/instruments/sqlite2pg.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | This will convert an Epics Instruments file in SQLite 4 | to one using PostgresQL ScanDB 5 | 6 | requires epicsscan to be installed 7 | """ 8 | 9 | from epicsscan import ScanDB, InstrumentDB 10 | 11 | # Name of the Sqlite3 file 12 | fname = '13BMC_Setup_2024-3.ein' 13 | 14 | # Database connection information 15 | dbname = 'mydb' 16 | username = 'db_user' 17 | password = 'something_super_secret' 18 | host = 'cars4.cars.aps.anl.gov' 19 | 20 | def sqlite2pg(fname, dbname, username, password, host): 21 | sdb = ScanDB(fname, server='sqlite3') 22 | pdb = ScanDB(dbname, server='postgresql', host=host, 23 | user=username, password=password) 24 | 25 | s_idb = InstrumentDB(sdb) 26 | p_idb = InstrumentDB(pdb) 27 | 28 | for row in sdb.get_rows('pv'): 29 | pdb.add_row('pv', name=row.name, notes=row.notes, 30 | pvtype_id=row.pvtype_id) 31 | 32 | for row in s_idb.get_all_instruments(): 33 | instname = row.name 34 | print("Get Sqlite Instrument ", instname) 35 | inst = s_idb.get_instrument(instname) 36 | for posname in s_idb.get_positionlist(instname): 37 | pvals = s_idb.get_position_vals(instname, posname) 38 | 39 | if p_idb.get_instrument(instname) is None: 40 | print("Add PG Instrument ", instname) 41 | p_idb.add_instrument(instname, pvs=list(pvals.keys())) 42 | print("Save position ", instname, posname, pvals) 43 | p_idb.save_position(instname, posname, pvals) 44 | 45 | print("done") 46 | -------------------------------------------------------------------------------- /examples/microscope/microscope.yaml: -------------------------------------------------------------------------------- 1 | calibration: 2 | - [default, -0.41, 0.41] 3 | - ['0.75', -0.787, 0.787] 4 | - ['1.5', -0.52, 0.52] 5 | - [10 um pixels, 0.01, 0.01] 6 | camera_id: 18457226 7 | camera_type: pyspin 8 | center_with_fine_stages: false 9 | image_folder: Sample_Images 10 | instrument: SampleStage 11 | lamp: {ctrlpv: '13IDE:DAC1_2.VAL', max_val: 5.0, min_val: 0.0, step: 0.1} 12 | offline_instrument: IDE_Microscope 13 | offline_xyzmotors: ['13IDE:m1.VAL', '13IDE:m2.VAL', '13IDE:m3.VAL'] 14 | xyzmotors: ['13XRM:m4.VAL', '13XRM:m5.VAL', '13XRM:m6.VAL'] 15 | overlays: 16 | - [circle, 10.0, 0.5, 0.5, 2.0, 187, 255, 119] 17 | - [scalebar, 100.0, 0.88, 0.97, 2.0, 255, 255, 128] 18 | scandb_credentials: ESCAN_CREDENTIALS 19 | stages: 20 | - ['13XRM:m1.VAL', Fine Stages, finex, -1.0, 4, 11.8, 1] 21 | - ['13XRM:m2.VAL', Fine Stages, finey, -1.0, 4, 2.28, 1] 22 | - ['13XRM:m5.VAL', Coarse Stages, x, 1.0, 3, 55.1, 1] 23 | - ['13XRM:m6.VAL', Coarse Stages, y, 1.0, 3, 55.1, 1] 24 | - ['13XRM:m4.VAL', Focus, z, 1.0, 3, 55.1, 1] 25 | - ['13XRM:m3.VAL', Theta, theta, 1.0, 3, 99.5, 0] 26 | title: 13IDE SampleStage 27 | verify_erase: true 28 | verify_move: true 29 | verify_overwrite: true 30 | workdir: U:\2019.3\_Setup 31 | zmq_push: true 32 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=45", "setuptools_scm>=6.2"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.setuptools_scm] 6 | write_to = "epicsapps/version.py" 7 | version_scheme = "post-release" 8 | 9 | [tool.setuptools.packages.find] 10 | include = ["epicsapps", "epicsapps/areadetector", 11 | "epicsapps/areadetector", "epicsapps/icons", "epicsapps/instruments", 12 | "epicsapps/ionchamber", "epicsapps/microscope", "epicsapps/pvlogger", 13 | "epicsapps/stripchart", "epicsapps/utils"] 14 | [project] 15 | name = "epicsapps" 16 | dynamic = ["version"] 17 | requires-python = ">= 3.9" 18 | description = "A collection of applications using PyEpics" 19 | readme = "README.md" 20 | authors = [ 21 | {name = "Matthew Newville", email = "newville@cars.uchicago.edu"} 22 | ] 23 | license = {file = "LICENSE"} 24 | keywords = ["epics", "experimental data acquistion"] 25 | classifiers = [ 26 | "Development Status :: 5 - Production/Stable", 27 | "Intended Audience :: Developers", 28 | "License :: OSI Approved :: MIT License", 29 | "Operating System :: OS Independent", 30 | "Programming Language :: Python :: 3.9", 31 | "Programming Language :: Python :: 3.10", 32 | "Programming Language :: Python :: 3.11", 33 | "Programming Language :: Python :: 3.12", 34 | "Programming Language :: Python :: 3.13", 35 | ] 36 | dependencies = ['pyepics', 37 | 'sqlalchemy', 38 | 'psycopg2', 39 | 'pyyaml', 40 | 'numpy', 41 | 'matplotlib', 42 | 'pytz', 43 | 'python-dateutil', 44 | 'wxpython>=4.1.0', 45 | 'wxmplot', 46 | 'wxutils', 47 | 'lmfit', 48 | 'pyshortcuts>=1.9.5', 49 | 'xraydb', 50 | 'tabulate', 51 | 'toml', 52 | 'tomli', 53 | 'opencv-python', 54 | ] 55 | 56 | [project.scripts] 57 | epicsapps = "epicsapps:run_epicsapps" 58 | epics_adviewer = "epicsapps:run_adviewer" 59 | epics_instruments = "epicsapps:run_instruments" 60 | epics_stripchart = "epicsapps:run_stripchart" 61 | 62 | [project.urls] 63 | Homepage = " https://github.com/pyepics/epicsapps" 64 | Documentation = "https://pyepics.github.io/epicsapps/" 65 | Tracker = "https://github.com/pyepics/epicsapps/issues" 66 | 67 | [project.optional-dependencies] 68 | dev = ["build", "twine"] 69 | doc = ["Sphinx", "sphinx-copybutton", "sphinxcontrib-video"] 70 | test = ["pytest", "pytest-cov", "coverage"] 71 | all = ["epicsapps[dev, doc, test]"] 72 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy>=1.15 2 | wxmplot>=0.9.38 3 | wxpython>=4.0.3 4 | wxutils>=0.2.3 5 | matplotlib>=3.0 6 | sqlalchemy>=0.9 7 | lmfit>=0.9.13 8 | pyshortcuts>=1.6 9 | pyepics>=3.4.0 10 | yaml>=5.0 11 | xraydb 12 | pip 13 | pillow 14 | pyfai 15 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import setuptools 3 | if __name__ == "__main__": 4 | setuptools.setup() 5 | -------------------------------------------------------------------------------- /tt.yaml: -------------------------------------------------------------------------------- 1 | end_datetime: '2025-01-27 09:00:00' 2 | folder: pvlogx 3 | instruments: [BPM Foil] 4 | pvs: ['RF-ACIS:FePermit:Sect1To35IdM.VAL | Shutter Permit | ', 'S:SRcurrentAI.VAL 5 | | Storage Ring Current | 0.002', 'S13ID:DSID:GapM.VAL | ID Gap, ID-C/D | 0.001', 6 | 'S13ID:DSID:TaperGapM.VAL | ID Gap Taper, ID-C/D | 0.001', 'S13ID:USID:GapM.VAL 7 | | ID Gap, ID-E | 0.001', 'S13ID:USID:TaperGapM.VAL | ID Gap Taper, ID-E | 0.001'] 8 | workdir: C:/Users/xas_user/Documents/GitHub/epicsapps 9 | --------------------------------------------------------------------------------