diff --git a/README.md b/README.md index cfcfe4a..0706d0d 100644 --- a/README.md +++ b/README.md @@ -1,453 +1,10 @@ -e3-ecmc_plugin_fft +ecmc_plugin_motion ====== -ESS Site-specific EPICS module : ecmcPlugin_FFT +Plugin designed for commisioning and troubleshooting of motion axes. +Motion data are sampled, buffered and exposed to epics as waveforms. -A shared library with FFT functionalities loadable into ecmc: -https://github.com/epics-modules/ecmc (or local ess fork https://github.com/icshwi/ecmc). -Configuration is made through ecmccfg: -https://github.com/paulscherrerinstitute/ecmccfg (ot local ess fork https://github.com/icshwi/ecmccfg) -FFT:s are calculated with the kissfft lib: -https://github.com/mborgerding/kissfft - -# Introduction - -The main functionality of this plugin is to make FFT analysis of ecmc data. Most data in ecmc is accessible: -1. EtherCAT data ("ec0.s1.AI_1" or "ec0.s2.mm.CH1_ARRAY") -2. Motion data ("ax1.actpos") -3. PLC data ("plcs.plc0.static.testvalue") - -Precautions have been taken in order to disturb ecmc real time thread as little as possible: -1. All CPU intensive calculations are handled in a low prio work thread (not ecmc thread). -2. Communication to epics is made via a dedicated asynPortDriver (not the same that ecmc uses). - -See below for configuration options and the different modes supported by this ecmc plugin. - -## Loading of FFT plugin in ecmc: -A plugin is loaded by the ecmccfg command loadPlugin: -https://github.com/icshwi/ecmccfg/blob/master/scripts/loadPlugin.cmd - -Example: -``` - ${SCRIPTEXEC} ${ecmccfg_DIR}loadPlugin.cmd, "PLUGIN_ID=0,FILE=libecmcPlugin_FFT.so,CONFIG='DBG_PRINT=1;SOURCE=ax1.actpos;RM_LIN=1;', REPORT=1 - dbLoadRecords(ecmcPluginFFT.template,"P=$(IOC):,INDEX=0, NELM=${FFT_NELM}") -``` -This plugin supports multiple loading. For each load of the plugin a new FFT object will be created. In order to access these plugins, from plc:s or EPICS records, they can be accessed by an index. The first FFT plugin will have index 0. The next loaded FFT plugin will have index 1... - -Note: If another plugin is loaded in between the loading of FFT plugins, it will have no affect on these FFT indexes (so the FFT index is _not_ the same as plugin index). - -## Configuration - -The different available configuration settings: -* SOURCE= source variable : Sets source variable for FFT (example: ec0.s1.AI_1). This config is mandatory. -* DBG_PRINT=1/0 : Enables/disables printouts from plugin, default = disabled. -* NFFT= nfft : Data points to collect, default = 4096. -* SCALE=scale : Apply scale to input data, default = 1.0. -* RM_DC=1/0 : Remove DC offset of input data (SOURCE), default = disabled. -* RM_LIN=1/0 : Remove linear component input data (SOURCE), default = disabled. -* ENABLE=1/0 : Enable data acq. and calcs (can be controlled over asyn), default = disabled. -* MODE=CONT/TRIGG : Continious or triggered mode, defaults to TRIGG -* RATE=rate in hz : fft data sample rate in hz (must be lower than ecmc rate and (ecmc_rate/fft_rate)=integer), default = ecmc rate. -* BREAKTABLE= EPICS breaktable : Apply breaktable to raw value. - -Example configuration string: -``` -"SOURCE=ax1.poserr;MODE=TRIGG;DBG_PRINT=1;ENABLE=1;" -``` - -#### SOURCE (mandatory) -The data source is defined by setting the SOURCE option in the plugin configuration string. -This configuration is mandatory. - -Example: Axis 1 actpos (See RM_LIN config below) -``` -"DBG_PRINT=1;SOURCE=ax1.actpos;RM_LIN=1;" - -``` -Example: Ethercat slave 1 analog input ch1 -``` -"DBG_PRINT=1;SOURCE=ec0.s1.AI_1;" -``` -#### DBG_PRINT (default: disabled) -Enable/disable printouts from plugin can be made bu setting the "DBG_PRINT" option. - -Example: Disable -``` -"DBG_PRINT=0;SOURCE=ax1.poserr;" -``` - -#### NFFT (default: 4096) -Defines number of samples for each measurement. - -Note: Must be a n² number.. - -Example: 1024 -``` -"NFFT=1024;DBG_PRINT=0;SOURCE=ax1.poserr;" -``` -#### SCALE (default 1.0) -Apply custom scale to input data. - -Example: 5.0 -``` -"SCALE=5.0;NFFT=1024;DBG_PRINT=0;SOURCE=ax1.poserr;" -``` -### RM_DC -Remove DC of input signal. Default is disabled. - -Example: Remove DC offset -``` -"RM_DC=1;SCALE=1;NFFT=1024;DBG_PRINT=0;SOURCE=ax1.poserr;" - -``` -### RM_LIN -Remove linear component of input signal. Default is disabled. -The linear component is calculated by least square method. -Could be usefull for values that increase, like actual position. - -Example: Remove linear component -``` -"RM_LIN=1;SCALE=1;NFFT=1024;DBG_PRINT=0;SOURCE=ax1.poserr;" -``` -#### ENABLE (default: disabled) -Enable data acq. and FFT calcs. The default settings is disabled so needs to be enabled from plc or over asyn in order to start calculations. - -Example: Enable at startup by config -``` -"ENABLE=1;RM_DC=1;SCALE=1;NFFT=1024;DBG_PRINT=0;SOURCE=ax1.poserr;" -``` -Example: Enable FFT index 0 from EPICS: -``` -caput IOC_TEST:Plugin-FFT0-Enable 1 -``` -Example: Enable FFT index 0 from ecmc PLC code: -``` -fft_enable(0,1) -``` - -#### MODE (default: TRIGG) -The FFT module can operate in two different modes: -* CONTINIOUS (CONT) -* TRIGGERED (TRIGG) - -Continious mode: -1. Clear data buffers -2. Wait for enable (from plc or asyn/epics record or configuration(see above)) -3. Data acqusition starts -4. When buffers are full a worker thread will be triggered to calculate the FFT. -5. FTT results are sent by callback over asyn to epics records -6. goto 1. - -Note: data will be "lost" during calculation time. - -Triggered mode: -1. Clear data buffers -2. Wait for enable (from plc or asyn/epics record or configuration(see above)) -3. Wait for trigger (from plc or asyn/epics record) -3. Data acqusition starts -4. When buffers are full a worker thread will be triggered to calculate the FFT. -5. FTT results are sent by callback over asyn to epics records -6. goto 1. - -Example: Mode triggered -``` -"MODE=TRIGG;ENABLE=1;RM_DC=1;SCALE=1;NFFT=1024;DBG_PRINT=0;SOURCE=ax1.poserr;" -``` -Example: Mode from EPICS record -``` -# CONT -caput IOC_TEST:Plugin-FFT0-Mode-RB 1 -# TRIGG -caput IOC_TEST:Plugin-FFT0-Mode-RB 2 -``` -Note: The record is a output record with readback so can both be read and written to. - -#### RATE (default: the ecmc rate of the selected data source) -Sets the sample rate of the raw input data (from data source). The default value is the ecmc rate for that data source. -Note: only a lower and "integer" division of sample rate can be defined. - -Example: Rate = 100Hz -``` -"RATE=100;MODE=TRIGG;ENABLE=1;RM_DC=1;SCALE=1;NFFT=1024;DBG_PRINT=0;SOURCE=ax1.poserr;" -``` - -#### BREAKTABLE (default: no break table) - -Note: The break table must be added in EPICS - -Example: Apply BREAKTABLE=typeTelemess33020_21877 to raw value -``` -"...;BREAKTABLE=typeTelemess33020_21877;" -``` - -Example: Load custom breaktable in EPICS -``` -dbLoadRecords ./bptBreakTable.dbd -``` - -Example: breaktable (in a bptBreakTable.dbd) -``` - # - breaktable(typeTelemess33020_21877) { - 2502.48 0 - 4058.89 1.5 - 6164.64 3 - 8880.74 4.5 - 11993.57 6 - 15259.00 7.5 - 18371.84 9 - 21271.05 10.5 - 24658.54 12 - 27344.13 13.5 - 29297.28 15 - } - -``` - -## EPICS records -Each FFT plugin object will create a new asynportdriver-port named "PLUGIN.FFT" (index is explaine above). -The reason for a dedicated asynport is to disturb ecmc as little as possible. -The plugin contains a template file, "ecmcPluginFFT.template", that will make most information availbe from records: -* Rawdata array (ro) -* FFT amplitude array (result) (ro) -* FFT x axis (frequencies) (ro) -* Status (ro) -* Mode (rw) -* Sample rate (ro) -* Enable cmd (rw) -* Trigger cmd (rw) -* NFFT (ro) - -The available records from this template file can be listed by the cmd (here two FFT plugins loaded): -``` -dbgrep *FFT* -IOC_TEST:Plugin-FFT0-SampleRate-Act -IOC_TEST:Plugin-FFT1-SampleRate-Act -IOC_TEST:Plugin-FFT0-Mode-RB -IOC_TEST:Plugin-FFT1-Mode-RB -IOC_TEST:Plugin-FFT0-Source -IOC_TEST:Plugin-FFT0-Raw-Data-Act -IOC_TEST:Plugin-FFT0-Spectrum-Amp-Act -IOC_TEST:Plugin-FFT0-Spectrum-X-Axis-Act -IOC_TEST:Plugin-FFT1-Source -IOC_TEST:Plugin-FFT1-Raw-Data-Act -IOC_TEST:Plugin-FFT1-Spectrum-Amp-Act -IOC_TEST:Plugin-FFT1-Spectrum-X-Axis-Act -IOC_TEST:Plugin-FFT0-Enable -IOC_TEST:Plugin-FFT0-Trigg -IOC_TEST:Plugin-FFT1-Enable -IOC_TEST:Plugin-FFT1-Trigg -IOC_TEST:Plugin-FFT0-stat -IOC_TEST:Plugin-FFT0-NFFT -IOC_TEST:Plugin-FFT1-stat -IOC_TEST:Plugin-FFT1-NFFT -``` -Note: The FFT asynparameters will not be visible by the ecmcReport iocsh command since the FFT records belong to another port. - -## PLC interface - -### PLC Functions - -1. "fft_clear(arg0);" double fft_clear(index) : Clear/resets all buffers fft[index]. -2. "fft_enable(arg0, arg1);" double fft_enable(index, enable) : Set enable for fft[index]. -3. "fft_trigg(arg0);" double fft_trigg(index) : Trigg new measurement for fft[index]. Will clear buffers. -4. "fft_mode(arg0, arg1);" double fft_mode(index, mode) : Set mode Cont(1)/Trigg(2) for fft[index]. -5. "fft_stat(arg0);" double fft_stat(index) : Get status of fft (NO_STAT, IDLE, ACQ, CALC) for fft[index]. - -### PLC Constants: - -These constants are for use togheter with the above listed PLC functions: -1. "fft_CONT" = 1: FFT Mode: Continious -2. "fft_TRIGG" = 2: FFT Mode :Triggered -3. "fft_NO_STAT" = 0: FFT Status: Invalid state -4. "fft_IDLE" = 1: FFT Status: Idle state (waiting for trigger) -5. "fft_ACQ" = 2: FFT Status: Acquiring data -6. "fft_CALC" = 3: FFT Status: Calculating result - -## Example script -An example script can be found in the iocsh directory of this repo. -This example load 2 FFT plugin objects: -1. SOURCE="plcs.plc0.static.sineval" which is a plc variable updated as a sinus with default freq 5 Hz. -2. SOURCE="ecmc.thread.latency.max". This is the execution latency of ecmc (in nanoseconds). - -Start one FFT calc over EPICS records: -``` -# Enable -caput IOC_TEST:Plugin-FFT0-Enable 1 -# Trigg a new calc -caput IOC_TEST:Plugin-FFT1-Trigg 1 -``` - -#### Example input signal -Epics record: IOC_TEST:Plugin-FFT0-Raw-Data-Act - -|![ecmc dep](docs/ecmcPLC5HzRAW.png)| -| :---: | -|**Figure 1** Raw data. | - -#### Example FFT result -Epics records: -* X = IOC_TEST:Plugin-FFT0-Spectrum-X-Axis-Act -* Y = IOC_TEST:Plugin-FFT0-Spectrum-Amp-Act - -|![ecmc dep](docs/ecmcPLC5HzFFT.png)| -| :---: | -|**Figure 2** Resulting FFT amplitude. | - -#### ecmc PLC code for example: -``` -static.time:=ec_get_time()/1E9; -static.sineval:=sin(2*pi*${FREQ=5}*static.time); -``` - -## FFT GUI - -### FFT GUI (FFT and rawdata plots + controls) -A simple tool, [ecmcFFTMainGui.py](tools/ecmcFFTMainGui.py), to visualize the calculated spectrum, rawdata and also plugin controls can be found in the tools directory. The GUI connects to the plugin records over pypics framwork. -The gui are included in the ecmccomgui repo: -https://github.com/anderssandstrom/ecmccomgui - -Example: ecmcFFTMainGui.py help printout -``` -python ecmcFFTMainGui.py -ecmcFFTMainGui: Plots waveforms of FFT data (updates on Y data callback). -python ecmcFFTMainGui.py -: Ioc prefix ('IOC_TEST:') - : Id of fft plugin ('0') -example : python ecmcFFTMainGui.py 'IOC_TEST:' '0' -Will connect to Pvs: Plugin-FFT-* -``` - -Example: Start ecmcFFMainTGui.py for: -* predix="IOC_TEST:" -* fftPluginId=0 (the first loaded FFT plugin in the ioc) -``` -python ecmcFFTMainGui.py IOC_TEST: 0 -``` -![ecmcFFTMainGui.py](docs/gui/ecmcFFTMainGui.png) - -### FFT GUI (only FFT plot) - -A simple tool, [ecmcFFTGui.py](tools/ecmcFFTGui.py), to visualize the calculated spectrum can be found in the tools directory. -The GUI connects to the plugin records over pypics framwork. - -Example: ecmcFFTGui.py help printout -``` -python ecmcFFTGui.py -ecmcFFTGui: Plots waveforms of FFT data (updates on Y data callback). -python ecmcFFTGui.py -example: python ecmcFFTGui.py IOC_TEST:Plugin-FFT1-Spectrum-X-Axis-Act IOC_TEST:Plugin-FFT1-Spectrum-Amp-Act -``` - -Example: Start ecmcFFTGui.py for spectrum amplitude and freq. waveform pvs -``` -python ecmcFFTGui.py IOC_TEST:Plugin-FFT1-Spectrum-X-Axis-Act IOC_TEST:Plugin-FFT1-Spectrum-Amp-Act -``` -![ecmcFFTMainGui.py](docs/gui/ecmcFFTGui.png) - -### Array GUI (generic waveform plt) -A simple generic tool, [ecmcFFTGui.py](tools/ecmcFFTGui.py), to visualize wavforms. -The GUI connects to the plugin records over pypics framwork. - -Example: ecmcArrayGui.py help printout -``` -python ecmcArrayGui.py -ecmcArrayGui: Plots waveforms data (updates on data callback). -python ecmcArrayGui.py -example: python ecmcArrayGui.py IOC_TEST:Plugin-FFT0-Raw-Data-Act -``` - -Example: Start ecmcArrayGui.py for raw data wavform -``` -python ecmcArrayGui.py IOC_TEST:Plugin-FFT0-Raw-Data-Act -``` -![ecmcFFTMainGui.py](docs/gui/ecmcArrayGui.png) - -### Needed packages: -* python 3.5 -* epics -* PyQt5 -* numpy -* matplotlib - - -## Plugin info - -``` -Plugin info: - Index = 1 - Name = ecmcPlugin_FFT - Description = FFT plugin for use with ecmc. - Option description = - DBG_PRINT=<1/0> : Enables/disables printouts from plugin, default = disabled. - SOURCE= : Sets source variable for FFT (example: ec0.s1.AI_1). - NFFT= : Data points to collect, default = 4096. - SCALE=<1/0> : Apply scale, default = disabled. - RM_DC=<1/0> : Remove DC offset of input data (SOURCE), default = disabled. - ENABLE=<1/0> : Enable data acq. and calcs (can be controlled over asyn), default = disabled. - MODE= : Continious or triggered mode, defaults to TRIGG - RATE= : fft data sample rate in hz (must be lower than ecmc rate and (ecmc_rate/fft_rate)=integer), default = ecmc rate. - Filename = /epics/base-7.0.3.1/require/3.1.2/siteMods/ecmcPlugin_FFT/master/lib/linux-arm/libecmcPlugin_FFT.so - Config string = SOURCE=ecmc.thread.latency.max;DBG_PRINT=0;NFFT=1024; - Version = 1 - Interface version = 512 (ecmc = 512) - max plc funcs = 64 - max plc func args = 10 - max plc consts = 64 - Construct func = @0xb5022044 - Enter realtime func = @0xb5022090 - Exit realtime func = @0xb502203c - Realtime func = @0xb5022034 - Destruct func = @0xb502206c - dlhandle = @0x1d9de28 - Plc functions: - funcs[00]: - Name = "fft_clear(arg0);" - Desc = double fft_clear(index) : Clear/reset fft[index]. - Arg count = 1 - func = @0xb5022094 - funcs[01]: - Name = "fft_enable(arg0, arg1);" - Desc = double fft_enable(index, enable) : Set enable for fft[index]. - Arg count = 2 - func = @0xb50220b0 - funcs[02]: - Name = "fft_trigg(arg0);" - Desc = double fft_trigg(index) : Trigg new measurement for fft[index]. Will clear buffers. - Arg count = 1 - func = @0xb50220d4 - funcs[03]: - Name = "fft_mode(arg0, arg1);" - Desc = double fft_mode(index, mode) : Set mode Cont(1)/Trigg(2) for fft[index]. - Arg count = 2 - func = @0xb50220f0 - funcs[04]: - Name = "fft_stat(arg0);" - Desc = double fft_stat(index) : Get status of fft (NO_STAT, IDLE, ACQ, CALC) for fft[index]. - Arg count = 1 - func = @0xb5022114 - Plc constants: - consts[00]: - Name = "fft_CONT" = 1.000 - Desc = FFT Mode: Continious - consts[01]: - Name = "fft_TRIGG" = 2.000 - Desc = FFT Mode :Triggered - consts[02]: - Name = "fft_NO_STAT" = 0.000 - Desc = FFT Status: Invalid state - consts[03]: - Name = "fft_IDLE" = 1.000 - Desc = FFT Status: Idle state (waiting for trigger) - consts[04]: - Name = "fft_ACQ" = 2.000 - Desc = FFT Status: Acquiring data - consts[05]: - Name = "fft_CALC" = 3.000 - Desc = FFT Status: Calculating result - -``` diff --git a/iocsh/pvs.log b/iocsh/pvs.log index 32922bb..fcc801e 100644 --- a/iocsh/pvs.log +++ b/iocsh/pvs.log @@ -28,6 +28,8 @@ IOC_TEST:Axis1-VelAct IOC_TEST:Axis1-PosSet IOC_TEST:Axis1-PosErr IOC_TEST:Axis1-PLC-Err +IOC_TEST:Plg-Mtn0-BuffSze +IOC_TEST:Plg-Mtn0-ElmCnt IOC_TEST:MCU-AppMode IOC_TEST:MCU-ErrId IOC_TEST:MCU-ThdLatMin @@ -170,12 +172,24 @@ IOC_TEST:m0-Dom-WC-Zero IOC_TEST:m0-Dom-WC-Incomplete IOC_TEST:m0-Dom-WC-Complete IOC_TEST:m0-Stat-OK -REQMOD:raspberrypi-3352:exit -REQMOD:raspberrypi-3352:MODULES -REQMOD:raspberrypi-3352:VERSIONS -REQMOD:raspberrypi-3352:MOD_VER +REQMOD:raspberrypi-1801:exit +REQMOD:raspberrypi-1801:MODULES +REQMOD:raspberrypi-1801:VERSIONS +REQMOD:raspberrypi-1801:MOD_VER IOC_TEST:Axis1-Arr-Stat IOC_TEST:Axis1-PLC-Expr-RB +IOC_TEST:Plg-Mtn0-PosAct-Arr +IOC_TEST:Plg-Mtn0-PosSet-Arr +IOC_TEST:Plg-Mtn0-PosErr-Arr +IOC_TEST:Plg-Mtn0-Time-Arr +IOC_TEST:Plg-Mtn0-Ena-Arr +IOC_TEST:Plg-Mtn0-EnaAct-Arr +IOC_TEST:Plg-Mtn0-Bsy-Arr +IOC_TEST:Plg-Mtn0-Exe-Arr +IOC_TEST:Plg-Mtn0-TrjSrc-Arr +IOC_TEST:Plg-Mtn0-EncSrc-Arr +IOC_TEST:Plg-Mtn0-AtTrg-Arr +IOC_TEST:Plg-Mtn0-ErrId-Arr IOC_TEST:MCU-ErrMsg IOC_TEST:MCU-Updated IOC_TEST:m0s001-Enc01-LtchCmd @@ -196,6 +210,8 @@ IOC_TEST:Axis1-DIR_ IOC_TEST:Axis1-ErrRst IOC_TEST:Axis1-HomProc IOC_TEST:Axis1-MtnCmdData +IOC_TEST:Plg-Mtn0-Mde-RB +IOC_TEST:Plg-Mtn0-Cmd-RB IOC_TEST:m0s001-Stat IOC_TEST:m0s002-Stat IOC_TEST:Axis1-MR-ErrId @@ -206,6 +222,7 @@ IOC_TEST:Axis1-CfgDLLM-En-RB IOC_TEST:Axis1-Stat IOC_TEST:Axis1-ErrId IOC_TEST:Axis1-WrnId +IOC_TEST:Plg-Mtn0-Stat IOC_TEST:m0-Stat IOC_TEST:m0-SlvCntr IOC_TEST:m0-MemmapCntr @@ -228,17 +245,18 @@ IOC_TEST:m0-SlvRsp IOC_TEST:m0-Dom-WC IOC_TEST:m0s001-Enc01-LtchRst IOC_TEST:Axis1-Cmd_ -REQMOD:raspberrypi-3352:BaseVersion -REQMOD:raspberrypi-3352:require_VER -REQMOD:raspberrypi-3352:ecmccfg_VER -REQMOD:raspberrypi-3352:asyn_VER -REQMOD:raspberrypi-3352:exprtk_VER -REQMOD:raspberrypi-3352:motor_VER -REQMOD:raspberrypi-3352:ruckig_VER -REQMOD:raspberrypi-3352:ecmc_VER +REQMOD:raspberrypi-1801:BaseVersion +REQMOD:raspberrypi-1801:require_VER +REQMOD:raspberrypi-1801:ecmccfg_VER +REQMOD:raspberrypi-1801:asyn_VER +REQMOD:raspberrypi-1801:exprtk_VER +REQMOD:raspberrypi-1801:motor_VER +REQMOD:raspberrypi-1801:ruckig_VER +REQMOD:raspberrypi-1801:ecmc_VER IOC_TEST:m0s001-HWType IOC_TEST:m0s002-HWType IOC_TEST:Axis1-MsgTxt +REQMOD:raspberrypi-1801:ecmc_plugin_motion_VER IOC_TEST:m0s001-Drv01-Stat IOC_TEST:m0s001-Enc01-Stat IOC_TEST:m0s001-Stat_ @@ -287,6 +305,10 @@ IOC_TEST:Axis1-TgtVelCmd IOC_TEST:Axis1-Id IOC_TEST:MCU-Cfg-AX1-NxtObjId IOC_TEST:MCU-Cfg-AX-FrstObjId +IOC_TEST:MCU-Cfg-PLG{Index}-NxtObjId +IOC_TEST:MCU-Cfg-PLG-FrstObjId +IOC_TEST:Plg-Mtn0-AxCmd-RB +IOC_TEST:Plg-Mtn0-SmpHz-RB IOC_TEST:MCU-Cfg-Eng-Mode IOC_TEST:m0s001-Enc01-LchAutRstSp IOC_TEST:m0s002-BO01 @@ -315,6 +337,8 @@ IOC_TEST:Axis1-PLC-EnaCmd IOC_TEST:Axis1-CmdFrmPLCCmd IOC_TEST:Axis1-SftLimBwdEna IOC_TEST:Axis1-SftLimFwdEna +IOC_TEST:Plg-Mtn0-EnaCmd-RB +IOC_TEST:Plg-Mtn0-TrgCmd-RB IOC_TEST:MCU-ErrRst IOC_TEST:Axis1-MCU1-asyn IOC_TEST:MCU-Cmd diff --git a/tools/ecmcMotionMainGui.py b/tools/ecmcMotionMainGui.py new file mode 100644 index 0000000..56a505f --- /dev/null +++ b/tools/ecmcMotionMainGui.py @@ -0,0 +1,827 @@ +#************************************************************************* +# Copyright (c) 2020 European Spallation Source ERIC +# ecmc is distributed subject to a Software License Agreement found +# in file LICENSE that is included with this distribution. +# +# ecmcMtnMainGui.py +# +# Created on: October 6, 2020 +# Author: Anders Sandström +# +# Plots two waveforms (x vs y) updates for each callback on the y-pv +# +#************************************************************************* + +import sys +import os +import epics +from PyQt5.QtWidgets import * +from PyQt5 import QtWidgets +from PyQt5.QtCore import * +from PyQt5.QtGui import * +import numpy as np +import matplotlib +matplotlib.use("Qt5Agg") +from matplotlib.figure import Figure +from matplotlib.animation import TimedAnimation +from matplotlib.lines import Line2D +from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas +from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar +import matplotlib.pyplot as plt +import threading + +# List of pv names +pvlist= [ 'BuffSze', + 'ElmCnt', + 'PosAct-Arr', + 'PosSet-Arr', + 'PosErr-Arr', + 'Time-Arr', + 'Ena-Arr', + 'EnaAct-Arr', + 'Bsy-Arr', + 'Exe-Arr', + 'TrjSrc-Arr', + 'EncSrc-Arr', + 'AtTrg-Arr', + 'ErrId-Arr', + 'Mde-RB', + 'Cmd-RB', + 'Stat', + 'AxCmd-RB', + 'SmpHz-RB', + 'TrgCmd-RB', + 'EnaCmd-RB' ] + +pvmiddlestring='Plg-Mtn' + +class comSignal(QObject): + data_signal = pyqtSignal(object) + +class ecmcMtnMainGui(QtWidgets.QDialog): + def __init__(self,prefix=None,mtnPluginId=None): + super(ecmcMtnMainGui, self).__init__() + #Mtn plugin + self.pvnames={} + self.pvs={} + self.pv_signal_cbs={} + self.data={} + + # + + self.offline = False + self.pvPrefixStr = prefix + self.pvPrefixOrigStr = prefix # save for restore after open datafile + self.mtnPluginId = mtnPluginId + self.mtnPluginOrigId = mtnPluginId + self.allowSave = False + self.path = '.' + self.unitRawY = "[]" + self.unitSpectY = "[]" + self.labelSpectY = "Amplitude" + self.labelRawY = "Raw" + self.title = "" + self.NMtn = 1024 + self.sampleRate = 1000 + self.sampleRateValid = False + self.MtnYDataValid = False + self.MtnXDataValid = False + self.RawYDataValid = False + self.RawXDataValid = False + if prefix is None or mtnPluginId is None: + self.offline = True + self.pause = True + self.enable = False + else: + #Check for connection else go offline + self.buildPvNames() + connected = self.pvs['BuffSze'].wait_for_connection(timeout=2) + if connected: + self.offline = False + self.pause = False + else: + self.offline = True + self.pause = True + self.enable = False + + # Callbacks through signals + #self.comSignalX = comSignal() + #self.comSignalX.data_signal.connect(self.callbackFuncX) +# + #self.comSignalSpectY = comSignal() + #self.comSignalSpectY.data_signal.connect(self.callbackFuncSpectY) + #self.comSignalRawData = comSignal() + #self.comSignalRawData.data_signal.connect(self.callbackFuncrawData) + #self.comSignalEnable = comSignal() + #self.comSignalEnable.data_signal.connect(self.callbackFuncEnable) + #self.comSignalMode = comSignal() + #self.comSignalMode.data_signal.connect(self.callbackFuncMode) + #self.comSignalBuffIdAct = comSignal() + #self.comSignalBuffIdAct.data_signal.connect(self.callbackFuncBuffIdAct) + self.startupDone=False + + self.pause = 0 + + # Data + #self.dataX = None + #self.spectY = None + #self.rawdataY = None + #self.rawdataX = None + #self.enable = None + + + self.createWidgets() + #self.connectPvs() + self.setStatusOfWidgets() + self.resize(1000,850) + + return + + def createWidgets(self): + + self.figure = plt.figure() + self.plottedLineSpect = None + self.plottedLineRaw = None + self.axSpect = None + self.axRaw = None + self.canvas = FigureCanvas(self.figure) + self.toolbar = NavigationToolbar(self.canvas, self) + self.pauseBtn = QPushButton(text = 'pause') + self.pauseBtn.setFixedSize(100, 50) + self.pauseBtn.clicked.connect(self.pauseBtnAction) + self.pauseBtn.setStyleSheet("background-color: green") + self.openBtn = QPushButton(text = 'open data') + self.openBtn.setFixedSize(100, 50) + self.openBtn.clicked.connect(self.openBtnAction) + self.saveBtn = QPushButton(text = 'save data') + self.saveBtn.setFixedSize(100, 50) + self.saveBtn.clicked.connect(self.saveBtnAction) + self.enableBtn = QPushButton(text = 'enable Mtn') + self.enableBtn.setFixedSize(100, 50) + self.enableBtn.clicked.connect(self.enableBtnAction) + self.triggBtn = QPushButton(text = 'trigg Mtn') + self.triggBtn.setFixedSize(100, 50) + self.triggBtn.clicked.connect(self.triggBtnAction) + self.zoomBtn = QPushButton(text = 'auto zoom') + self.zoomBtn.setFixedSize(100, 50) + self.zoomBtn.clicked.connect(self.zoomBtnAction) + self.modeCombo = QComboBox() + self.modeCombo.setFixedSize(100, 50) + self.modeCombo.currentIndexChanged.connect(self.newModeIndexChanged) + self.modeCombo.addItem("CONT") + self.modeCombo.addItem("TRIGG") + self.progressBar = QProgressBar() + self.progressBar.reset() + self.progressBar.setMinimum(0) + self.progressBar.setMaximum(100) #100% + self.progressBar.setValue(0) + self.progressBar.setFixedHeight(20) + + # Fix layout + self.setGeometry(300, 300, 900, 700) + + layoutVert = QVBoxLayout() + layoutVert.addWidget(self.toolbar) + layoutVert.addWidget(self.canvas) + + layoutControl = QHBoxLayout() + layoutControl.addWidget(self.pauseBtn) + layoutControl.addWidget(self.enableBtn) + layoutControl.addWidget(self.triggBtn) + layoutControl.addWidget(self.modeCombo) + layoutControl.addWidget(self.zoomBtn) + layoutControl.addWidget(self.saveBtn) + layoutControl.addWidget(self.openBtn) + + frameControl = QFrame(self) + frameControl.setFixedHeight(70) + frameControl.setLayout(layoutControl) + + + layoutVert.addWidget(frameControl) + layoutVert.addWidget(self.progressBar) + self.setLayout(layoutVert) + + def setStatusOfWidgets(self): + self.saveBtn.setEnabled(self.allowSave) + if self.offline: + self.enableBtn.setStyleSheet("background-color: grey") + self.enableBtn.setEnabled(False) + self.pauseBtn.setStyleSheet("background-color: grey") + self.pauseBtn.setEnabled(False) + self.modeCombo.setEnabled(False) + self.triggBtn.setEnabled(False) + self.setWindowTitle("ecmc Mtn Main plot: Offline") + else: + self.modeCombo.setEnabled(True) + # Check actual value of pvs + enable = self.pvs['EnaCmd-RB'].get() + if enable is None: + print("pvs['EnaCmd-RB'].get() failed") + return + if(enable>0): + self.enableBtn.setStyleSheet("background-color: green") + self.enable = True + else: + self.enableBtn.setStyleSheet("background-color: red") + self.enable = False + + #self.sourceStr = self.pvSource.get(as_string=True) + #if self.sourceStr is None: + # print("pvSource.get() failed") + # return + + self.sampleRate = self.pvs['SmpHz-RB'].get() + if self.sampleRate is None: + print("pvs['SmpHz-RB'].get() failed") + return + self.sampleRateValid = True + + self.mode = self.pvs['Mde-RB'].get() + if self.mode is None: + print("pvs['Mde-RB'].get() failed") + return + + self.modeStr = "NO_MODE" + self.triggBtn.setEnabled(False) # Only enable if mode = TRIGG = 2 + if self.mode == 1: + self.modeStr = "CONT" + self.modeCombo.setCurrentIndex(self.mode-1) # Index starta t zero + + if self.mode == 2: + self.modeStr = "TRIGG" + self.triggBtn.setEnabled(True) + self.modeCombo.setCurrentIndex(self.mode-1) # Index starta t zero + + self.setWindowTitle("ecmc Mtn Main plot: prefix=" + self.pvPrefixStr + " , mtnId=" + str(self.mtnPluginId) + + ", rate=" + str(self.sampleRate)) + + def buildPvNames(self): + print('HEHEHEHHEH') + # Pv names based on structure: Plugin-Mtn- + for pv in pvlist: + self.pvnames[pv]=self.buildPvName(pv) + if self.pvnames[pv] is None: + raise RuntimeError("pvname must not be 'None'") + if len(self.pvnames[pv])==0: + raise RuntimeError("pvname must not be ''") + self.pvs[pv] = epics.PV(self.pvnames[pv]) + self.pv_signal_cbs[pv] = comSignal() + + # Signal callbacks (update gui) + # replace any '-' with '_' since '-' not allowed in funcion names + sig_cb_func=getattr(self,'sig_cb_' + pv.replace('-','_')) + self.pv_signal_cbs[pv].data_signal.connect(sig_cb_func) + + # Pv monitor callbacks + mon_cb_func=getattr(self,'on_change_' + pv.replace('-','_')) + self.pvs[pv].add_callback(mon_cb_func) + + QCoreApplication.processEvents() + + def buildPvName(self, suffixname): + return self.pvPrefixStr + pvmiddlestring + str(self.mtnPluginId) + '-' + suffixname + + ###### Pv monitor callbacks + def on_change_BuffSze(self,pvname=None, value=None, char_value=None,timestamp=None, **kw): + print('On Change') + self.pv_signal_cbs['BuffSze'].data_signal.emit(value) + + def on_change_ElmCnt(self,pvname=None, value=None, char_value=None,timestamp=None, **kw): + print('On Change') + self.pv_signal_cbs['ElmCnt'].data_signal.emit(value) + + def on_change_PosAct_Arr(self,pvname=None, value=None, char_value=None,timestamp=None, **kw): + print('On Change') + self.pv_signal_cbs['PosAct-Arr'].data_signal.emit(value) + + def on_change_PosSet_Arr(self,pvname=None, value=None, char_value=None,timestamp=None, **kw): + print('On Change') + self.pv_signal_cbs['PosSet-Arr'].data_signal.emit(value) + + def on_change_PosErr_Arr(self,pvname=None, value=None, char_value=None,timestamp=None, **kw): + print('On Change') + self.pv_signal_cbs['PosErr-Arr'].data_signal.emit(value) + + def on_change_Time_Arr(self,pvname=None, value=None, char_value=None,timestamp=None, **kw): + print('On Change') + self.pv_signal_cbs['Time-Arr'].data_signal.emit(value) + + def on_change_Ena_Arr(self,pvname=None, value=None, char_value=None,timestamp=None, **kw): + print('On Change') + self.pv_signal_cbs['Ena-Arr'].data_signal.emit(value) + + def on_change_EnaAct_Arr(self,pvname=None, value=None, char_value=None,timestamp=None, **kw): + print('On Change') + self.pv_signal_cbs['EnaAct-Arr'].data_signal.emit(value) + + def on_change_Bsy_Arr(self,pvname=None, value=None, char_value=None,timestamp=None, **kw): + print('On Change') + self.pv_signal_cbs['Bsy-Arr'].data_signal.emit(value) + + def on_change_Exe_Arr(self,pvname=None, value=None, char_value=None,timestamp=None, **kw): + print('On Change') + self.pv_signal_cbs['Exe-Arr'].data_signal.emit(value) + + def on_change_TrjSrc_Arr(self,pvname=None, value=None, char_value=None,timestamp=None, **kw): + print('On Change') + self.pv_signal_cbs['TrjSrc-Arr'].data_signal.emit(value) + + def on_change_EncSrc_Arr(self,pvname=None, value=None, char_value=None,timestamp=None, **kw): + print('On Change') + self.pv_signal_cbs['EncSrc-Arr'].data_signal.emit(value) + + def on_change_AtTrg_Arr(self,pvname=None, value=None, char_value=None,timestamp=None, **kw): + print('On Change') + self.pv_signal_cbs['AtTrg-Arr'].data_signal.emit(value) + + def on_change_ErrId_Arr(self,pvname=None, value=None, char_value=None,timestamp=None, **kw): + print('On Change') + self.pv_signal_cbs['ErrId-Arr'].data_signal.emit(value) + + def on_change_Mde_RB(self,pvname=None, value=None, char_value=None,timestamp=None, **kw): + print('On Change') + self.pv_signal_cbs['Mde-RB'].data_signal.emit(value) + + def on_change_Cmd_RB(self,pvname=None, value=None, char_value=None,timestamp=None, **kw): + print('On Change') + self.pv_signal_cbs['Cmd-RB'].data_signal.emit(value) + + def on_change_Stat(self,pvname=None, value=None, char_value=None,timestamp=None, **kw): + print('On Change') + self.pv_signal_cbs['Stat'].data_signal.emit(value) + + def on_change_AxCmd_RB(self,pvname=None, value=None, char_value=None,timestamp=None, **kw): + print('On Change') + self.pv_signal_cbs['AxCmd-RB'].data_signal.emit(value) + + def on_change_SmpHz_RB(self,pvname=None, value=None, char_value=None,timestamp=None, **kw): + print('On Change') + self.pv_signal_cbs['SmpHz-RB'].data_signal.emit(value) + + def on_change_TrgCmd_RB(self,pvname=None, value=None, char_value=None,timestamp=None, **kw): + print('On Change') + self.pv_signal_cbs['TrgCmd-RB'].data_signal.emit(value) + + def on_change_EnaCmd_RB(self,pvname=None, value=None, char_value=None,timestamp=None, **kw): + print('On Change') + self.pv_signal_cbs['EnaCmd-RB'].data_signal.emit(value) + +# def onChangePvMode(self,pvname=None, value=None, char_value=None,timestamp=None, **kw): +# if self.pause: +# return +# self.comSignalMode.data_signal.emit(value) +# +# def onChangePvEnable(self,pvname=None, value=None, char_value=None,timestamp=None, **kw): +# if self.pause: +# return +# self.comSignalEnable.data_signal.emit(value) +# +# def onChangeX(self,pvname=None, value=None, char_value=None,timestamp=None, **kw): +# if self.pause: +# return +# self.comSignalX.data_signal.emit(value) +# +# def onChangePvSpectY(self,pvname=None, value=None, char_value=None,timestamp=None, **kw): +# if self.pause: +# return +# self.comSignalSpectY.data_signal.emit(value) +# +# def onChangePvrawData(self,pvname=None, value=None, char_value=None,timestamp=None, **kw): +# if self.pause: +# return +# self.comSignalRawData.data_signal.emit(value) +# +# def onChangePvBuffIdAct(self,pvname=None, value=None, char_value=None,timestamp=None, **kw): +# if self.pause: +# return +# self.comSignalBuffIdAct.data_signal.emit(value) + + ###### Signal callbacks + # dbgrep *Plg*Mtn* + # IOC_TEST:Plg-Mtn0-BuffSze + # IOC_TEST:Plg-Mtn0-ElmCnt + # IOC_TEST:Plg-Mtn0-PosAct-Arr + # IOC_TEST:Plg-Mtn0-PosSet-Arr + # IOC_TEST:Plg-Mtn0-PosErr-Arr + # IOC_TEST:Plg-Mtn0-Time-Arr + # IOC_TEST:Plg-Mtn0-Ena-Arr + # IOC_TEST:Plg-Mtn0-EnaAct-Arr + # IOC_TEST:Plg-Mtn0-Bsy-Arr + # IOC_TEST:Plg-Mtn0-Exe-Arr + # IOC_TEST:Plg-Mtn0-TrjSrc-Arr + # IOC_TEST:Plg-Mtn0-EncSrc-Arr + # IOC_TEST:Plg-Mtn0-AtTrg-Arr + # IOC_TEST:Plg-Mtn0-ErrId-Arr + # IOC_TEST:Plg-Mtn0-Mde-RB + # IOC_TEST:Plg-Mtn0-Cmd-RB + # IOC_TEST:Plg-Mtn0-Stat + # IOC_TEST:Plg-Mtn0-AxCmd-RB + # IOC_TEST:Plg-Mtn0-SmpHz-RB + # IOC_TEST:Plg-Mtn0-TrgCmd-RB + # IOC_TEST:Plg-Mtn0-EnaCmd-RB + + def sig_cb_BuffSze(self,value): + print('HEPP') + + def sig_cb_ElmCnt(self,value): + print('HEPP') + + def sig_cb_PosAct_Arr(self,value): + print('HEPP') + + def sig_cb_PosSet_Arr(self,value): + print('HEPP') + + def sig_cb_PosErr_Arr(self,value): + print('HEPP') + + def sig_cb_Time_Arr(self,value): + if(np.size(value)) > 0: + self.dataX = value + self.MtnXDataValid = True + return + + + def sig_cb_Ena_Arr(self,value): + print('HEPP') + + def sig_cb_EnaAct_Arr(self,value): + print('HEPP') + + def sig_cb_Bsy_Arr(self,value): + print('HEPP') + + def sig_cb_Exe_Arr(self,value): + print('HEPP') + + def sig_cb_TrjSrc_Arr(self,value): + print('HEPP') + + def sig_cb_EncSrc_Arr(self,value): + print('HEPP') + + def sig_cb_AtTrg_Arr(self,value): + print('HEPP') + + def sig_cb_ErrId_Arr(self,value): + print('HEPP') + + def sig_cb_Mde_RB(self,value): + + if value < 1 or value> 2: + self.modeStr = "NO_MODE" + print('callbackFuncMode: Error Invalid mode.') + return + + self.mode = value + self.modeCombo.setCurrentIndex(self.mode-1) # Index starta t zero + + if self.mode == 1: + self.modeStr = "CONT" + self.triggBtn.setEnabled(False) # Only enable if mode = TRIGG = 2 + + if self.mode == 2: + self.modeStr = "TRIGG" + self.triggBtn.setEnabled(True) + + return + + + def sig_cb_Cmd_RB(self,value): + print('HEPP') + + def sig_cb_Stat(self,value): + print('HEPP') + + def sig_cb_AxCmd_RB(self,value): + print('HEPP') + + def sig_cb_SmpHz_RB(self,value): + print('HEPP') + + def sig_cb_TrgCmd_RB(self,value): + print('HEPP') + + def sig_cb_EnaCmd_RB(self,value): + self.enable = value + if self.enable: + self.enableBtn.setStyleSheet("background-color: green") + else: + self.enableBtn.setStyleSheet("background-color: red") + self.data['EnaCmd-RB'] = value + return + + def callbackFuncSpectY(self, value): + if(np.size(value)) > 0: + self.spectY = value + self.MtnYDataValid = self.RawXDataValid + self.plotAll() + return + + def callbackFuncrawData(self, value): + if(np.size(value)) > 0: + if (self.rawdataX is None or np.size(value) != np.size(self.rawdataY)) and self.sampleRateValid: + self.rawdataX = np.arange(-np.size(value)/self.sampleRate, 0, 1/self.sampleRate) + self.RawXDataValid = True + + self.rawdataY = value + self.RawYDataValid = True + self.plotAll() + return + + def callbackFuncBuffIdAct(self, value): + if self.NMtn is None: + return + if(self.NMtn>0): + self.progressBar.setValue(value/self.NMtn*100) + if value/self.NMtn*100 < 80 and value/self.NMtn*100 >1: + self.MtnYDataValid = False + self.RawYDataValid = False + return + + ###### Widget callbacks + def pauseBtnAction(self): + self.pause = not self.pause + if self.pause: + self.pauseBtn.setStyleSheet("background-color: red") + else: + self.pvPrefixStr = self.pvPrefixOrigStr # Restore if dataset was opened + self.mtnPluginId = self.mtnPluginOrigId # Restore if dataset was opened + self.buildPvNames() + + self.pauseBtn.setStyleSheet("background-color: green") + # Retrigger plots with newest values + self.comSignalSpectY.data_signal.emit(self.spectY) + self.comSignalRawData.data_signal.emit(self.rawdataY) + return + + def enableBtnAction(self): + self.enable = not self.enable + self.pvs['EnaCmd-RB'].put(self.enable) + if self.enable: + self.enableBtn.setStyleSheet("background-color: green") + else: + self.enableBtn.setStyleSheet("background-color: red") + return + + def triggBtnAction(self): + self.pvTrigg.put(True) + return + + def zoomBtnAction(self): + + if self.rawdataY is None: + return + if self.rawdataX is None: + return + if self.spectY is None: + return + if self.dataX is None: + return + if self.axSpect is None: + return + + # Spect + self.axSpect.autoscale(enable=True) + self.plotSpect(True) + # rawdata + self.plotRaw(True) + + return + + def newModeIndexChanged(self,index): + if index==0 or index==1: + if not self.offline and self.pvs['Mde-RB'] is not None: + self.pvs['Mde-RB'].put(index+1) + return + + def openBtnAction(self): + #if not self.offline: + # self.pause = 1 # pause while open if online + # self.pauseBtn.setStyleSheet("background-color: red") + # QCoreApplication.processEvents() + # + #fname = QFileDialog.getOpenFileName(self, 'Open file', self.path, "Data files (*.npz)") + #if fname is None: + # return + #if np.size(fname) != 2: + # return + #if len(fname[0])<=0: + # return + #self.path = os.path.dirname(os.path.abspath(fname[0])) + # + #npzfile = np.load(fname[0]) +# + ## verify scope plugin + #if npzfile['plugin'] != "Mtn": + # print ("Invalid data type (wrong plugin type)") + # return + # + ## File valid + #self.rawdataX = npzfile['rawdataX'] + #self.rawdataY = npzfile['rawdataY'] + #self.dataX = npzfile['spectX'] + #self.spectY = npzfile['spectY'] + #self.sampleRate = npzfile['sampleRate'] + #self.NMtn = npzfile['NMtn'] + #self.mode = npzfile['mode'] + #self.pvPrefixStr = str(npzfile['pvPrefixStr']) + #self.mtnPluginId = npzfile['mtnPluginId'] + #if 'unitRawY' in npzfile: + # self.unitRawY = str(npzfile['unitRawY']) + #if 'unitSpectY' in npzfile: + # self.unitSpectY = str(npzfile['unitSpectY']) + #if 'labelRawY' in npzfile: + # self.labelRawY = str(npzfile['labelRawY']) + #if 'labelSpectY' in npzfile: + # self.labelSpectY = str(npzfile['labelSpectY']) + #if 'title' in npzfile: + # self.title = str(npzfile['title']) +# + #self.buildPvNames() + # + ## trigg draw + #self.MtnYDataValid = True + #self.MtnXDataValid = True + #self.RawYDataValid = True + #self.RawXDataValid = True + #self.sampleRateValid = True + + + #self.comSignalMode.data_signal.emit(self.mode) + #self.comSignalX.data_signal.emit(self.dataX) + #self.comSignalSpectY.data_signal.emit(self.spectY) + #self.comSignalRawData.data_signal.emit(self.rawdataY) + + #self.setStatusOfWidgets() +# + #self.startupDone=True + #self.zoomBtnAction() + return + + def saveBtnAction(self): + #fname = QFileDialog.getSaveFileName(self, 'Save file', self.path, "Data files (*.npz)") + #if fname is None: + # return + #if np.size(fname) != 2: + # return + #if len(fname[0])<=0: + # return + ## Save all relevant data + #np.savez(fname[0], + # plugin = "Mtn", + # rawdataX = self.rawdataX, + # rawdataY = self.rawdataY, + # spectX = self.dataX, + # spectY = self.spectY, + # sampleRate = self.sampleRate, + # NMtn = self.NMtn, + # mode = self.mode, + # pvPrefixStr = self.pvPrefixStr, + # mtnPluginId = self.mtnPluginId, + # unitRawY = self.unitRawY, + # unitSpectY = self.unitSpectY, + # labelRawY = self.labelRawY, + # labelSpectY = self.labelSpectY, + # title = self.title + # ) +# + #self.path = os.path.dirname(os.path.abspath(fname[0])) + + return + + def plotAll(self): + print("self.MtnYDataValid = " + str(self.MtnYDataValid)) + print("self.MtnXDataValid = " + str(self.MtnXDataValid)) + print("self.RawYDataValid = " + str(self.RawYDataValid)) + print("self.RawXDataValid = " + str(self.RawXDataValid)) + + if self.MtnYDataValid and self.MtnXDataValid and self.RawYDataValid and self.RawXDataValid: + self.plotSpect() + self.plotRaw() + self.MtnYDataValid = False + self.RawYDataValid = False + + ###### Plotting + def plotSpect(self, autozoom=False): + if self.dataX is None: + return + if self.spectY is None: + return + + # create an axis for spectrum + if self.axSpect is None: + self.axSpect = self.figure.add_subplot(212) + + # plot data + if self.plottedLineSpect is not None: + self.plottedLineSpect.remove() + + self.plottedLineSpect, = self.axSpect.plot(self.dataX,self.spectY, 'b*-') + self.axSpect.grid(True) + + + self.axSpect.set_xlabel("Frequency [Hz]") + self.axSpect.set_ylabel(self.labelSpectY + ' ' +self.unitSpectY) + + if autozoom: + ymin = np.min(self.spectY) + ymax = np.max(self.spectY) + # ensure different values + if ymin == ymax: + ymin = ymin - 1 + ymax = ymax + 1 + range = ymax - ymin + ymax += range * 0.1 + ymin -= range * 0.1 + xmin = np.min(self.dataX) + xmax = np.max(self.dataX) + if xmin == xmax: + xmin = xmin - 1 + xmax = xmax + 1 + range = xmax - xmin + xmax += range * 0.02 + xmin -= range * 0.02 + self.axSpect.set_ylim(ymin,ymax) + self.axSpect.set_xlim(xmin,xmax) + + # refresh canvas + self.canvas.draw() + self.axSpect.autoscale(enable=False) + + def plotRaw(self, autozoom=False): + if self.rawdataY is None: + return + + if self.rawdataX is None: + return + + # create an axis for spectrum + if self.axRaw is None: + self.axRaw = self.figure.add_subplot(211) + + + # plot data + if self.plottedLineRaw is not None: + self.plottedLineRaw.remove() + + self.plottedLineRaw, = self.axRaw.plot(self.rawdataX,self.rawdataY, 'b*-') + self.axRaw.grid(True) + + self.axRaw.set_xlabel('Time [s]') + self.axRaw.set_ylabel(self.labelRawY + ' ' + self.unitRawY) + self.axRaw.set_title(self.title) + + if autozoom: + ymin = np.min(self.rawdataY) + ymax = np.max(self.rawdataY) + # ensure different values + if ymin == ymax: + ymin=ymin-1 + ymax=ymax+1 + range = ymax - ymin + ymax += range * 0.1 + ymin -= range * 0.1 + xmin = np.min(self.rawdataX) + xmax = np.max(self.rawdataX) + if xmin == xmax: + xmin = xmin - 1 + xmax = xmax + 1 + range = xmax - xmin + xmax += range * 0.02 + xmin -= range * 0.02 + self.axRaw.set_ylim(ymin,ymax) + self.axRaw.set_xlim(xmin,xmax) + + # refresh canvas + self.canvas.draw() + self.allowSave = True + self.saveBtn.setEnabled(True) + self.axRaw.autoscale(enable=False) + +def printOutHelp(): + print("ecmcMtnMainGui: Plots waveforms of Mtn data (updates on Y data callback). ") + print("python ecmcMtnMainGui.py ") + print(": Ioc prefix ('IOC_TEST:')") + print(" : Id of mtn plugin ('0')") + print("example : python ecmcMotionMainGui.py 'IOC_TEST:' '0'") + print("Will connect to Pvs: Plg-Mtn-*") + +if __name__ == "__main__": + import sys + prefix = None + mtnid = None + if len(sys.argv) == 1: + prefix = None + mtnid = None + elif len(sys.argv) == 3: + prefix = sys.argv[1] + mtnid = int(sys.argv[2]) + else: + printOutHelp() + sys.exit() + app = QtWidgets.QApplication(sys.argv) + window=ecmcMtnMainGui(prefix=prefix,mtnPluginId=mtnid) + window.show() + sys.exit(app.exec_())