Files
PBSwissMX/python/PBTuning.py
2018-09-17 16:14:50 +02:00

615 lines
20 KiB
Python
Executable File

#!/usr/bin/env python
# *-----------------------------------------------------------------------*
# | |
# | Copyright (c) 2016 by Paul Scherrer Institute (http://www.psi.ch) |
# | |
# | Author Thierry Zamofing (thierry.zamofing@psi.ch) |
# *-----------------------------------------------------------------------*
'''
tuning functions for deltatau:
For now it does 10 step move and uploads the data
other available tuning progs on the powerbrick are:
analyzerautotunemove
autotunecalc
autotunemove
chirpmove
currentautotunecalc
currentstep
filtercalculation
openloopchirp +
openloopsine +
openlooptestmove
othertrajectory
parabolicmove
randommove
sinesweep +
sinusoidal +
stepmove
user_gantry_crosscoupled.h
usertrajectory
Modes:
1: sine bode open loop
2: chirp bode open loop
3: sine bode closed loop
4: chirp bode closed loop
TODO:
use openloopsine to create a bode diagram of the 'strecke'
'''
import os, sys, json, time
import numpy as np
import matplotlib as mpl
#mpl.use('GTKAgg')
import matplotlib.pyplot as plt
import subprocess as sprc
import telnetlib
from scipy.signal.waveforms import chirp
from scipy import signal
from utilities import *
class PBTuning:
tuneDir='/opt/ppmac/tune/'
def __init__(self,args):
self.args=args
def do_command(self,cmd,*param):
args=self.args
host=args.host
cmd=('ssh','root@'+host, self.tuneDir+cmd)+tuple(map(str,param))
print(' '.join(cmd))
p=sprc.Popen(cmd, shell=False, stdin=sprc.PIPE, stdout=sprc.PIPE, stderr=sprc.PIPE)
res=p.wait()
print(res)
print(p.stderr.read())
print(p.stdout.read())
PBGatherPlot='/home/zamofing_t/scripts/PBGatherPlot'
try:
fnLoc=self.fnLoc
except AttributeError:
fnLoc = '/tmp/gather.txt'
cmd=(PBGatherPlot,'-m24','-v0','--host',host,'--dat',fnLoc)
p = sprc.Popen(cmd, shell=False, stdin=sprc.PIPE, stdout=sprc.PIPE, stderr=sprc.PIPE)
retval = p.wait()
print(p.stderr.read())
#print(p.stdout.read())
#self.meta = {'timebase': ServoPeriod*gather['Period']}
self.data = np.genfromtxt(fnLoc, delimiter=' ')
return self.data
def phase_amp(self,frq,rep):
try:
ax1=self.ax1
ax2=self.ax2
ax1.clear()
ax2.clear()
except AttributeError:
fig = plt.figure(); self.ax1=ax1 = fig.add_subplot(1, 1, 1)
fig = plt.figure(); self.ax2=ax2 = fig.add_subplot(1, 1, 1)
n = self.data.shape[0]
w=np.hamming(n)
res=np.ndarray((2,2))
col=('b-','g-')
for i in (0,1):
data=self.data[:,i]
avg=data.mean(0)
#data=data-data[0]
data=data-avg
data*=w
ax1.plot(data, col[i])
f = np.fft.fft(data)
ax2.plot(np.absolute(f) , col[i])
idx = int(rep)
bias = np.absolute(f[0] / n)
phase = np.angle(f[idx])
#ampl = np.absolute(f[idx]) * 2 / n
ampl = np.absolute(f[idx]) * 2 / w.sum()
res[i,:]=(ampl,phase)
plt.pause(.05)
return (res[1,0]/res[0,0],res[1,1]-res[0,1])
def bode_sine(self,openloop=True,motor=1,minFrq=1,maxFrq=300,numFrq=150,amp=10,file='/tmp/bode.npz'):
if os.path.isfile(file):
f=np.load(file)
bode=f['bode']
meta=f['meta'].item()
meta['file']=file
else:
#motor 1 maximum: 13750
#amp= percentage of maximum amplitude
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
frqLst=np.logspace(np.log10(minFrq),np.log10(maxFrq),numFrq)
n=len(frqLst)
#frqLst=(10,15,20,25,30)
bode=np.ndarray((n,3))
bode[:, 0]=frqLst
#for i in range(n):
for i in range(n-1,-1,-1):
frq=frqLst[i]
t=1
rep=max(1,frq*t)
self.fnLoc='/tmp/gather%d.txt'%i
if openloop:
data=self.do_command('openloopsine',motor,amp,frq,rep,0)
else:
data=self.do_command('sinusoidal',motor,amp,frq,rep,0)
data=data[:,(1,2)]
ax.clear()
avg=data.mean(0)
print(avg)
ax.plot(data[:, 0]-avg[0] , 'b-', label='input')
ax.plot(data[:, 1]-avg[1], 'g-', label='output')
plt.pause(.05)
bode[i,1:]=self.phase_amp(frq, rep)
print('frq %g ampl %g phase %g'%tuple(bode[i,:]))
plt.draw_all()
meta={'motor':motor,'date':time.asctime()}
np.savez_compressed(file, bode=bode, meta=meta)
meta['file']=file
self.bode_sine_plot(bode, meta)
def bode_sine_plot(self,bode,meta):
frq=bode[:,0]
db_mag=20*np.log10(bode[:,1])
phase=np.degrees(np.unwrap(bode[:,2]))# numpy.unwrap(p, discont=3.141592653589793, axis=-1)
if phase[0]>0: phase-=360
fig = plt.figure()
fig.canvas.set_window_title(os.path.basename(meta['file']))
ax = fig.add_subplot(2, 1, 1)
ax.semilogx(frq, db_mag,'.-') # Bode magnitude plot
ax.xaxis.set_label_text('dB Mag.')
plt.grid(True)
#ax.loglog(frqLst, bode[:,0],'.-') # Bode magnitude plot
ax = fig.add_subplot(2, 1, 2)
ax.semilogx(frq, phase,'.-') # Bode phase plot
ax.xaxis.set_label_text('phase')
plt.grid(True)
def bode_chirp(self,openloop=True,motor=1,minFrq=10,maxFrq=150,tSec=30.,amp=10,mode=1,file='/tmp/gather.npz'):
#amp= percentage of maximum amplitud
if os.path.isfile(file):
f=np.load(file)
data=f['data']
#try:
meta=f['meta'].item()
#except KeyError:
# meta={'motor': motor, 'date': time.asctime(), 'minFrq': 10, 'maxFrq': 150, 'tSec': 30, 'amp': amp}
# np.savez_compressed(file, data=data, meta=meta)
meta['file']=file
else:
mode=1
#Sys.ServoPeriod=0.2ms = 5kHz -> therefore factor 2000 for the time second->servoCnt
#don't know why the frequency must be scaled...
if openloop:
data=self.do_command('openloopchirp',motor,amp,minFrq/2.,maxFrq/2.,tSec*2000,mode,0)
else:
data=self.do_command('sinesweep', motor, amp, minFrq/2., maxFrq/2., tSec*2000, mode, 0)
data = data[:, (1, 2)]
meta={'motor':motor,'date':time.asctime(),'minFrq':minFrq,'maxFrq':maxFrq,'tSec':tSec,'amp':amp}
np.savez_compressed(file, data=data, meta=meta)
meta['file'] = file
self.bode_chirp_plot(data, meta,openloop)
def bode_chirp_plot(self, data, meta,openloop):
tSec=meta['tSec']
minFrq=meta['minFrq']
maxFrq=meta['maxFrq']
#n=25000
#t=np.linspace(0, tSec, 2*n+1, endpoint=True)
#c=chirp(t, f0=minFrq, f1=maxFrq, t1=tSec, phi=-90, method='logarithmic')
#plt.figure()
#plt.clf()
#plt.subplot(2, 1, 1)
#plt.plot(t, c)
#tstr="Logarithmic Chirp, f(0)=%g, f(%g)=%g"%(minFrq, tSec, maxFrq)
#plt.title(tstr)
#plt.subplot(2, 1, 2)
#plt.plot(t, minFrq*(maxFrq/minFrq)**(t/tSec), 'r')
## yscale('log')
#plt.grid(True)
#plt.ylabel('Frequency (Hz)')
#plt.xlabel('time (sec)')
if openloop:
n=1000
d=np.concatenate((np.ones(n-1)*data[0, 1],data[:, 1]))
d=np.convolve(d,np.ones(n),'valid')/n
data[:, 1]-=d
# 5..150 Hz
# 15 sec
c = data[:, 0]
o = data[:, 1]
o -= o[0]
else:
data=data-data[0,:]
c = data[:,0]
o=data[:,1]
t = np.linspace(0, tSec, data.shape[0], endpoint=True)
n=data.shape[0]/2
fig=plt.figure()
fig.canvas.set_window_title(os.path.basename(meta['file']+' raw'))
plt.plot(t, c)
plt.plot(t, o)
#bode_plot(o)
#plt.show()
#n samples
#t1 seconds
#ts=t1/(2*n)#samplingperiode =t[1]-t[0]
#w_k=2*pi*k/T
#w=2*np.pi*np.arange(n+1)/t1*(2*n)
f=np.arange(n+1)/tSec #Hz
#w*=2.*np.pi #rad/sec
fig=plt.figure()
fig.canvas.set_window_title(os.path.basename(meta['file']+' bode'))
ftX=np.fft.rfft(c)
ftY=np.fft.rfft(o)
i=int(minFrq*tSec); j=int(maxFrq*tSec); #print(w[i],w[j])
f=f[i:j+1]
ftX=ftX[i:j+1]
ftY=ftY[i:j+1]
ft=ftY/ftX
phase=np.angle(ft)
phase=np.degrees(np.unwrap(phase))
#magDb=10*np.log10(np.abs(ft)) #in decibel
mag=np.abs(ftY)/np.abs(ftX)
magDb=20*np.log10(mag) #in decibel (20=10*2: factor 2 because rfft only half)
#magDb=np.abs(ftY)/np.abs(ftX)
ax=plt.subplot(2,1,1)
ax.semilogx(f,magDb,'b') # Bode magnitude plot
#ax.plot(w,magDb) # Bode magnitude plot
ax.axvline(minFrq,c='k');ax.axvline(maxFrq,c='k')
ax.grid(True)
ax=plt.subplot(2,1,2)
ax.semilogx(f,phase,'b') # Bode phase plot
ax.yaxis.set_label_text('Amplitude [dB]')
#ax.set_ylim(phase[i],phase[j])
ax.grid(True)
ax.xaxis.set_label_text('Frequency [Hz]')
ax.yaxis.set_label_text('Phase [degree]')
#plt.axvline(x=0.22058956)
#plt.show()
#meta={'motor':motor,'date':time.asctime()}
#np.savez_compressed(file, bode=bode, meta=meta)
def bode_full_plot(self, mot,base):
import glob
fn='sine_ol_mot%d.npz'%mot
file=os.path.join(base,fn)
f=np.load(file)
bode=f['bode']
meta=f['meta'].item()
frq=bode[:,0]
mag=bode[:,1]
phase=np.unwrap(bode[:,2])
l=[0,len(frq)]
#for fn in ('chirp_ol_mot%da.npz','chirp_ol_mot%db.npz','chirp_ol_mot%dc.npz','chirp_ol_mot%dd.npz'):
# fn=fn%mot
# file=os.path.join(base,fn)
for file in sorted(glob.glob(os.path.join(base,'chirp_ol_mot%d*'%mot))):
print(os.path.basename(file))
f=np.load(file)
data=f['data']
meta=f['meta'].item()
tSec=meta['tSec']
minFrq=meta['minFrq']
maxFrq=meta['maxFrq']
amp=meta['amp']
n=1000
d=np.concatenate((np.ones(n-1)*data[0, 1],data[:, 1]))
d=np.convolve(d,np.ones(n),'valid')/n
data[:, 1]-=d
# 5..150 Hz
# 15 sec
c = data[:, 0]
o = data[:, 1]
o -= o[0]
n=data.shape[0]/2
ftX=np.fft.rfft(c)
ftY=np.fft.rfft(o)
i=int(minFrq*tSec); j=int(maxFrq*tSec); #print(w[i],w[j])
f=np.arange(n+1)/tSec #Hz
f=f[i:j+1]
ftX=ftX[i:j+1]
ftY=ftY[i:j+1]
ft=ftY/ftX
frq=np.concatenate((frq,f))
phase_=np.unwrap(np.angle(ft))
if phase_[0]>0:
phase_-=2*np.pi
phase=np.concatenate((phase,phase_))
mag=np.concatenate((mag,(np.abs(ftY)/np.abs(ftX))))
l.append(len(frq))
db_mag=20*np.log10(mag)
phase=np.degrees(phase)# numpy.unwrap(p, discont=3.141592653589793, axis=-1)
fig = plt.figure()
fig.canvas.set_window_title('full bode of motor %d'%mot)
ax = fig.add_subplot(2, 1, 1)
plt.title('bode of motor %d'%mot)
for i in range(len(l)-1):
ax.semilogx(frq[l[i]:l[i+1]], db_mag[l[i]:l[i+1]],'-') # Bode magnitude plot
ax.yaxis.set_label_text('dB ampl')
ax.set_xlim(1,2000)
plt.grid(True)
#ax.loglog(frqLst, bode[:,0],'.-') # Bode magnitude plot
ax = fig.add_subplot(2, 1, 2)
for i in range(len(l)-1):
ax.semilogx(frq[l[i]:l[i+1]], phase[l[i]:l[i+1]],'-')#,zorder=i) # Bode phase plot
ax.yaxis.set_label_text('phase')
ax.xaxis.set_label_text('frequency [Hz]')
ax.set_xlim(1,2000)
ax.set_ylim(-360,0)
plt.grid(True)
def bode_model_plot(self, mot,base):
self.bode_full_plot(mot,base)
fig=plt.gcf()
if mot==1:
db_mag1=17.3 #dB
mag1=10**(db_mag1/20)
f1=6.5 #Hz
w1=f1*2*np.pi #rad/sec
T1=1/w1
d1=.7 # daempfung =1 -> keine resonanz -> den1= np.poly1d([T1,1])**2
num1=np.poly1d([mag1])
den1 = np.poly1d([T1**2,2*T1*d1,1])
#first resonance frequency
f2=np.array([191,198])
d2=np.array([.05,.05])#daempfung
w2=f2*2*np.pi #rad/sec
T2=1/w2
num2 = np.poly1d([T2[0]**2,2*T2[0]*d2[0],1])
den2 = np.poly1d([T2[1]**2,2*T2[1]*d2[1],1])
mdl= signal.lti(num2, den2) #num denum
#bode(mdl)
#current loop 2nd order approx
f4=900.
d4=1 # daempfung =1 -> keine resonanz -> den1= np.poly1d([T1,1])**2
w4=f4*2*np.pi #rad/sec
T4=1/w4
num4 = np.poly1d([1.])
den4 = np.poly1d([T4**2,2*T4*d4,1])
#mdl= signal.lti(num4, den4) #num denum
#bode(mdl)
num=num1*num2*num4#*num3
den=den1*den2*den4#*den3
mdl= signal.lti(num, den) #num denum
print num,den
print mdl
elif mot==2:
# basic 1/s^2 system with damping an d resonance
db_mag1=17.3 #dB
mag1=10**(db_mag1/20)
f1=4.5 #Hz
w1=f1*2*np.pi #rad/sec
d1=.3 # daempfung =1 -> keine resonanz -> den1= np.poly1d([T1,1])**2
T1=1/w1
num1 = np.poly1d([mag1])
den1 = np.poly1d([T1**2,2*T1*d1,1])
#first resonance frequency
f2=np.array([57.8,63.8])
d2=np.array([.06,.1])#daempfung
w2=f2*2*np.pi #rad/sec
T2=1/w2
num2 = np.poly1d([T2[0]**2,2*T2[0]*d2[0],1])
den2 = np.poly1d([T2[1]**2,2*T2[1]*d2[1],1])
mdl= signal.lti(num2, den2) #num denum
#bode(mdl)
#second resonance frequency
f3=np.array([130,141])
d3=np.array([.05,.07])#daempfung
w3=f3*2*np.pi #rad/sec
T3=1/w3
num3 = np.poly1d([T3[0]**2,2*T3[0]*d3[0],1])
den3 = np.poly1d([T3[1]**2,2*T3[1]*d3[1],1])
#mdl= signal.lti(num3, den3) #num denum
#bode(mdl)
#second resonance frequency
f4=np.array([405,415])
d4=np.array([.02,.02])#daempfung
w4=f4*2*np.pi #rad/sec
T4=1/w4
num4 = np.poly1d([T4[0]**2,2*T4[0]*d4[0],1])
den4 = np.poly1d([T4[1]**2,2*T4[1]*d4[1],1])
#mdl= signal.lti(num3, den3) #num denum
#bode(mdl)
#current loop 2nd order approx
fc=900.
dc=1 # daempfung =1 -> keine resonanz -> den1= np.poly1d([T1,1])**2
wc=fc*2*np.pi #rad/sec
Tc=1/wc
numc = np.poly1d([1.])
denc = np.poly1d([Tc**2,2*Tc*dc,1])
#mdl= signal.lti(num4, den4) #num denum
#bode(mdl)
num=num1*num2*num3*num4*numc
den=den1*den2*den3*den4*denc
mdl= signal.lti(num, den) #num denum
bode(mdl)
w=np.logspace(0,3,1000)*2*np.pi
w,mag,phase = signal.bode(mdl,w)
f=w/(2*np.pi)
ax=fig.axes[0]
ax.semilogx(f, mag,'-k',lw=2) # Bode magnitude plot
ax=fig.axes[1]
ax.semilogx(f, phase,'-k',lw=2) # Bode phase plot
# tp print see also: print(np.poly1d([1,2,3], variable='s')), print(np.poly1d([1,2,3], r=True, variable='s'))
def bode(mdl):
w,mag,phase = signal.bode(mdl)
f=w/(2*np.pi)
fig = plt.figure()
ax = fig.add_subplot(2, 1, 1)
ax.semilogx(f,mag,'-') # Bode magnitude plot
ax.yaxis.set_label_text('dB ampl')
plt.grid(True)
ax = fig.add_subplot(2, 1, 2)
ax.semilogx(f,phase,'-') # Bode magnitude plot
ax.yaxis.set_label_text('phase')
ax.xaxis.set_label_text('frequency [Hz]')
plt.grid(True)
#plt.show()
if __name__=='__main__':
from argparse import ArgumentParser,RawDescriptionHelpFormatter
def parse_args():
'main command line interpreter function'
#usage: gpasciiCommunicator.py --host=PPMACZT84 myPowerBRICK.cfg
(h, t)=os.path.split(sys.argv[0]);cmd='\n '+(t if len(h)>3 else sys.argv[0])+' '
exampleCmd=('-n',
'-v15'
)
epilog=__doc__+'''
Examples:'''+''.join(map(lambda s:cmd+s, exampleCmd))+'\n '
parser=ArgumentParser(epilog=epilog,formatter_class=RawDescriptionHelpFormatter)
parser.add_argument('--plot', nargs='*')
parser.add_argument('--host', help='hostname', metavar='HOST')
parser.add_argument('--verbose', '-v', type=int, help='verbosity bits (see below)', default=0)
parser.add_argument('--dryrun', '-n', action='store_true', help='dryrun to stdout')
parser.add_argument('--mode', '-m', type=int, help='modes (see below)', default=1)
parser.add_argument('--mot', type=int, help='motor', default=1)
parser.add_argument('--dir', help='dir', default='')
args=parser.parse_args()
#plt.ion()
tune=PBTuning(args)
#data=self.do_command('openloopsine',motor,amp,frq,rep,0)
# #data=self.do_command('openloopchirp',motor,amp,minFrq,maxFrq,tSec*1000,mode,0)
#THIS IS A TEST TO DO 5 Hz during 10 seconds
#frq=5
#frq2=frq*1.1
#ts=10 #seconds
#5 kHz Servo
#ds=tune.do_command('openloopsine', 2, 20, frq, frq*ts, 0)
#dc=tune.do_command('openloopchirp', 2, 20, frq/2., frq2/2., ts*2000, 1, 0) #THIS IS NOT 10 Seconds!!!
#dc=tune.do_command('openloopchirp', 2, 10, 5, 6, 10*1000, 1, 0) #THIS IS NOT 10 Seconds!!!
#ds=ds-ds[0,:]
#dc=dc-dc[0,:]
#plt.figure()
#plt.plot(ds[:,0],'b')
#plt.plot(ds[:,1],'g')
#plt.figure()
#plt.plot(dc[:,0],'b')
#plt.plot(dc[:,1],'g')
#plt.show()
#return
base='/home/zamofing_t/Documents/prj/SwissFEL/epics_ioc_modules/ESB_MX/python/PBTuning/'
if args.dir is not None:
base=os.path.join(base,args.dir)
if not os.path.exists(base):
os.mkdir(base)
#file=os.path.join(base, 'sine_ol_mot_tst.npz')
#tune.bode_sine(openloop=True,file=file,motor=2, minFrq=20,maxFrq=200,numFrq=30)
#file=os.path.join(base, 'chirp_ol_mot_tst.npz')
#tune.bode_chirp(openloop=True, file=file, motor=2, minFrq=20,maxFrq=200,tSec=30.)
#plt.show()
#return
if args.plot:
#display bode plots
if args.plot[0]=='full':
tune.bode_full_plot(mot=1,base=base)
tune.bode_full_plot(mot=2,base=base)
plt.show()
return
if args.plot[0]=='model':
tune.bode_model_plot(mot=1,base=base)
tune.bode_model_plot(mot=2,base=base)
plt.show()
return
for fn in args.plot:
if os.path.basename(fn).startswith('sine_ol_mot'):
tune.bode_sine(openloop=True,file=fn)
if os.path.basename(fn).startswith('chirp_ol_mot'):
tune.bode_chirp(openloop=True,file=fn)
if os.path.basename(fn).startswith('sine_cl_mot'):
tune.bode_sine(openloop=False, file=fn)
if os.path.basename(fn).startswith('chirp_cl_mot'):
tune.bode_chirp(openloop=False, file=fn)
plt.show()
return
mode=args.mode
mot=args.mot
if mode==1:
ol=True
file=os.path.join(base,'sine_ol_mot%d.npz'%(mot))
#def bode_sine(self, openloop=True, motor=1, minFrq=1, maxFrq=300, numFrq=150, amp=10, file='/tmp/bode.npz'):
tune.bode_sine(openloop=True,file=file,motor=mot)
elif mode==2:
file=os.path.join(base,'chirp_ol_mot%d.npz'%(mot))
#def bode_chirp(self,openloop=True,motor=1,minFrq=10,maxFrq=150,tSec=30.,amp=10,mode=1,file='/tmp/gather.npz'):
#tune.bode_chirp(openloop=True,file=file,motor=mot, minFrq=10, maxFrq=300, tSec=30)
tune.bode_chirp(openloop=True,file=file[:-4]+'a.npz',motor=mot, minFrq=10, maxFrq=300, tSec=30)
tune.bode_chirp(openloop=True,file=file[:-4]+'b.npz',motor=mot,amp=50,minFrq=100,maxFrq=500, tSec=30)
tune.bode_chirp(openloop=True,file=file[:-4]+'c.npz',motor=mot,amp=50,minFrq=300,maxFrq=1500,tSec=10)
tune.bode_chirp(openloop=True,file=file[:-4]+'d.npz',motor=mot,amp=100,minFrq=300,maxFrq=2000,tSec=10)
elif mode==3:
ol=True
file=os.path.join(base,'sine_cl_mot%d.npz'%(mot))
tune.bode_sine(openloop=False,file=file,motor=mot)
elif mode==4:
file=os.path.join(base,'chirp_cl_mot%d.npz'%(mot))
tune.bode_chirp(openloop=False,file=file,motor=mot)
elif mode==100: #testing
file=os.path.join(base,'chirp_ol_mot%d.npz'%(mot))
#def bode_chirp(self,openloop=True,motor=1,minFrq=10,maxFrq=150,tSec=30.,amp=10,mode=1,file='/tmp/gather.npz'):
#tune.bode_chirp(openloop=True,file=file,motor=mot, minFrq=10, maxFrq=300, tSec=30)
tune.bode_chirp(openloop=True,file=file[:-4]+'TEST.npz',motor=mot,amp=100,minFrq=300,maxFrq=2000,tSec=10)
#tune.do_command('stepmove',1,100,500,0,0)
plt.show()
#------------------ Main Code ----------------------------------
#ssh_test()
ret=parse_args()
exit(ret)
#enable plc1
#./PBTuning.py --host SAR-CPPM-EXPMX1 --mode 1 --mot 1 --dir tmp
#enable plc1
#./PBTuning.py --host SAR-CPPM-EXPMX1 --mode 2 --mot 1 --tag tmp
#enable plc1
#./PBTuning.py --host SAR-CPPM-EXPMX1 --mode 1 --mot 2 --tag tmp
#enable plc1
#./PBTuning.py --host SAR-CPPM-EXPMX1 --mode 2 --mot 2 --tag tmp
#enable plc1
#./PBTuning.py --host SAR-CPPM-EXPMX1 --plot ext