add missing files from secop_psi
- all of them have to be checked! Change-Id: I89d55ca683d0b2710222f14c2c3cd42f8fbf3a1f
This commit is contained in:
parent
31be98c62e
commit
bbe70fb3cb
294
frappy_psi/adq_mr.py
Normal file
294
frappy_psi/adq_mr.py
Normal file
@ -0,0 +1,294 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Created on Tue Nov 26 15:42:43 2019
|
||||
|
||||
@author: tartarotti_d-adm
|
||||
"""
|
||||
|
||||
|
||||
import numpy as np
|
||||
import ctypes as ct
|
||||
import time
|
||||
from numpy import sqrt, arctan2, sin, cos
|
||||
|
||||
#from pylab import *
|
||||
|
||||
from scipy import signal
|
||||
|
||||
#ADQAPI = ct.cdll.LoadLibrary("ADQAPI.dll")
|
||||
ADQAPI = ct.cdll.LoadLibrary("libadq.so.0")
|
||||
|
||||
#For different trigger modes
|
||||
SW_TRIG = 1
|
||||
EXT_TRIG_1 = 2 #This external trigger does not work if the level of the trigger is very close to 0.5V. Now we have it close to 3V, and it works
|
||||
EXT_TRIG_2 = 7
|
||||
EXT_TRIG_3 = 8
|
||||
LVL_TRIG = 3
|
||||
INT_TRIG = 4
|
||||
LVL_FALLING = 0
|
||||
LVL_RISING = 1
|
||||
|
||||
#samples_per_record=16384
|
||||
ADQ_TRANSFER_MODE_NORMAL = 0x00
|
||||
ADQ_CHANNELS_MASK = 0x3
|
||||
|
||||
#f_LO = 40
|
||||
|
||||
def butter_lowpass(cutoff, sr, order=5):
|
||||
nyq = 0.5 * sr
|
||||
normal_cutoff = cutoff / nyq
|
||||
b, a = signal.butter(order, normal_cutoff, btype = 'low', analog = False)
|
||||
return b, a
|
||||
|
||||
|
||||
class Adq(object):
|
||||
max_number_of_channels = 2
|
||||
samp_freq = 2
|
||||
#ndecimate = 50 # decimation ratio (2GHz / 40 MHz)
|
||||
ndecimate = 50
|
||||
|
||||
def __init__(self, number_of_records, samples_per_record, bw_cutoff):
|
||||
self.number_of_records = number_of_records
|
||||
self.samples_per_record = samples_per_record
|
||||
self.bw_cutoff = bw_cutoff
|
||||
ADQAPI.ADQAPI_GetRevision()
|
||||
|
||||
# Manually set return type from some ADQAPI functions
|
||||
ADQAPI.CreateADQControlUnit.restype = ct.c_void_p
|
||||
ADQAPI.ADQ_GetRevision.restype = ct.c_void_p
|
||||
ADQAPI.ADQ_GetPtrStream.restype = ct.POINTER(ct.c_int16)
|
||||
ADQAPI.ADQControlUnit_FindDevices.argtypes = [ct.c_void_p]
|
||||
# Create ADQControlUnit
|
||||
self.adq_cu = ct.c_void_p(ADQAPI.CreateADQControlUnit())
|
||||
ADQAPI.ADQControlUnit_EnableErrorTrace(self.adq_cu, 3, '.')
|
||||
self.adq_num = 1
|
||||
|
||||
# Find ADQ devices
|
||||
ADQAPI.ADQControlUnit_FindDevices(self.adq_cu)
|
||||
n_of_ADQ = ADQAPI.ADQControlUnit_NofADQ(self.adq_cu)
|
||||
if n_of_ADQ != 1:
|
||||
raise ValueError('number of ADQs must be 1, not %d' % n_of_ADQ)
|
||||
|
||||
rev = ADQAPI.ADQ_GetRevision(self.adq_cu, self.adq_num)
|
||||
revision = ct.cast(rev,ct.POINTER(ct.c_int))
|
||||
print('\nConnected to ADQ #1')
|
||||
# Print revision information
|
||||
print('FPGA Revision: {}'.format(revision[0]))
|
||||
if (revision[1]):
|
||||
print('Local copy')
|
||||
else :
|
||||
print('SVN Managed')
|
||||
if (revision[2]):
|
||||
print('Mixed Revision')
|
||||
else :
|
||||
print('SVN Updated')
|
||||
print('')
|
||||
|
||||
ADQ_CLOCK_INT_INTREF = 0 #internal clock source
|
||||
ADQ_CLOCK_EXT_REF = 1 #internal clock source, external reference
|
||||
ADQ_CLOCK_EXT_CLOCK = 2 #External clock source
|
||||
ADQAPI.ADQ_SetClockSource(self.adq_cu, self.adq_num, ADQ_CLOCK_EXT_REF);
|
||||
|
||||
##########################
|
||||
# Test pattern
|
||||
#ADQAPI.ADQ_SetTestPatternMode(self.adq_cu, self.adq_num, 4)
|
||||
##########################
|
||||
# Sample skip
|
||||
#ADQAPI.ADQ_SetSampleSkip(self.adq_cu, self.adq_num, 1)
|
||||
##########################
|
||||
|
||||
# Set trig mode
|
||||
self.trigger = EXT_TRIG_1
|
||||
#trigger = LVL_TRIG
|
||||
success = ADQAPI.ADQ_SetTriggerMode(self.adq_cu, self.adq_num, self.trigger)
|
||||
if (success == 0):
|
||||
print('ADQ_SetTriggerMode failed.')
|
||||
if (self.trigger == LVL_TRIG):
|
||||
success = ADQAPI.ADQ_SetLvlTrigLevel(self.adq_cu, self.adq_num, -100)
|
||||
if (success == 0):
|
||||
print('ADQ_SetLvlTrigLevel failed.')
|
||||
success = ADQAPI.ADQ_SetTrigLevelResetValue(self.adq_cu, self.adq_num, 1000)
|
||||
if (success == 0):
|
||||
print('ADQ_SetTrigLevelResetValue failed.')
|
||||
success = ADQAPI.ADQ_SetLvlTrigChannel(self.adq_cu, self.adq_num, 1)
|
||||
if (success == 0):
|
||||
print('ADQ_SetLvlTrigChannel failed.')
|
||||
success = ADQAPI.ADQ_SetLvlTrigEdge(self.adq_cu, self.adq_num, LVL_RISING)
|
||||
if (success == 0):
|
||||
print('ADQ_SetLvlTrigEdge failed.')
|
||||
elif (self.trigger == EXT_TRIG_1) :
|
||||
success = ADQAPI.ADQ_SetExternTrigEdge(self.adq_cu, self.adq_num,2)
|
||||
if (success == 0):
|
||||
print('ADQ_SetLvlTrigEdge failed.')
|
||||
# success = ADQAPI.ADQ_SetTriggerThresholdVoltage(self.adq_cu, self.adq_num, trigger, ct.c_double(0.2))
|
||||
# if (success == 0):
|
||||
# print('SetTriggerThresholdVoltage failed.')
|
||||
print("CHANNEL:"+str(ct.c_int(ADQAPI.ADQ_GetLvlTrigChannel(self.adq_cu, self.adq_num))))
|
||||
self.setup_target_buffers()
|
||||
|
||||
def setup_target_buffers(self):
|
||||
# Setup target buffers for data
|
||||
self.target_buffers=(ct.POINTER(ct.c_int16 * self.samples_per_record * self.number_of_records)
|
||||
* self.max_number_of_channels)()
|
||||
for bufp in self.target_buffers:
|
||||
bufp.contents = (ct.c_int16 * self.samples_per_record * self.number_of_records)()
|
||||
|
||||
def deletecu(self):
|
||||
# Only disarm trigger after data is collected
|
||||
ADQAPI.ADQ_DisarmTrigger(self.adq_cu, self.adq_num)
|
||||
ADQAPI.ADQ_MultiRecordClose(self.adq_cu, self.adq_num);
|
||||
# Delete ADQControlunit
|
||||
ADQAPI.DeleteADQControlUnit(self.adq_cu)
|
||||
|
||||
def start(self):
|
||||
"""start datat acquisition"""
|
||||
# samples_per_records = samples_per_record/number_of_records
|
||||
# Change number of pulses to be acquired acording to how many records are taken
|
||||
# Start acquisition
|
||||
ADQAPI.ADQ_MultiRecordSetup(self.adq_cu, self.adq_num,
|
||||
self.number_of_records,
|
||||
self.samples_per_record)
|
||||
|
||||
ADQAPI.ADQ_DisarmTrigger(self.adq_cu, self.adq_num)
|
||||
ADQAPI.ADQ_ArmTrigger(self.adq_cu, self.adq_num)
|
||||
|
||||
def getdata(self):
|
||||
"""wait for aquisition to be finished and get data"""
|
||||
#start = time.time()
|
||||
while(ADQAPI.ADQ_GetAcquiredAll(self.adq_cu,self.adq_num) == 0):
|
||||
time.sleep(0.001)
|
||||
#if (self.trigger == SW_TRIG):
|
||||
# ADQAPI.ADQ_SWTrig(self.adq_cu, self.adq_num)
|
||||
#mid = time.time()
|
||||
status = ADQAPI.ADQ_GetData(self.adq_cu, self.adq_num, self.target_buffers,
|
||||
self.samples_per_record * self.number_of_records, 2,
|
||||
0, self.number_of_records, ADQ_CHANNELS_MASK,
|
||||
0, self.samples_per_record, ADQ_TRANSFER_MODE_NORMAL);
|
||||
#print(time.time()-mid,mid-start)
|
||||
if not status:
|
||||
raise ValueError('no succesS from ADQ_GetDATA')
|
||||
# Now this is an array with all records, but the time is artificial
|
||||
data = []
|
||||
for ch in range(2):
|
||||
onedim = np.frombuffer(self.target_buffers[ch].contents, dtype=np.int16)
|
||||
data.append(onedim.reshape(self.number_of_records, self.samples_per_record) / float(2**14)) # 14 bits ADC
|
||||
return data
|
||||
|
||||
def acquire(self):
|
||||
self.start()
|
||||
return self.getdata()
|
||||
'''
|
||||
def average(self, data):
|
||||
#Average over records
|
||||
return [data[ch].sum(axis=0) / self.number_of_records for ch in range(2)]
|
||||
|
||||
def iq(self, channel, f_LO):
|
||||
newx = np.linspace(0, self.samples_per_record /2, self.samples_per_record)
|
||||
s0 = channel /((2**16)/2)*0.5*np.exp(1j*2*np.pi*f_LO/(1e3)*newx)
|
||||
I0 = s0.real
|
||||
Q0 = s0.imag
|
||||
return I0, Q0
|
||||
|
||||
|
||||
def fitting(self, data, f_LO, ti, tf):
|
||||
# As long as data[0] is the pulse
|
||||
si = 2*ti #Those are for fitting the pulse
|
||||
sf = 2*tf
|
||||
phase = np.zeros(self.number_of_records)
|
||||
amplitude = np.zeros(self.number_of_records)
|
||||
offset = np.zeros(self.number_of_records)
|
||||
|
||||
for i in range(self.number_of_records):
|
||||
phase[i], amplitude[i] = sineW(data[0][i][si:sf],f_LO*1e-9,ti,tf)
|
||||
offset[i] = np.average(data[0][i][si:sf])
|
||||
return phase, amplitude, offset
|
||||
|
||||
|
||||
def waveIQ(self, channel,ti,f_LO):
|
||||
#channel is not the sample data
|
||||
t = np.linspace(0, self.samples_per_record /2, self.samples_per_record + 1)[:-1]
|
||||
si = 2*ti # Again that is where the wave pulse starts
|
||||
cwi = np.zeros((self.number_of_records,self.samples_per_record))
|
||||
cwq = np.zeros((self.number_of_records,self.samples_per_record))
|
||||
iq = np.zeros((self.number_of_records,self.samples_per_record))
|
||||
q = np.zeros((self.number_of_records,self.samples_per_record))
|
||||
for i in range(self.number_of_records):
|
||||
cwi[i] = np.zeros(self.samples_per_record)
|
||||
cwq[i] = np.zeros(self.samples_per_record)
|
||||
cwi[i] = amplitude[i]*sin(t[si:]*f_LO*1e-9*2*np.pi+phase[i]*np.pi/180)+bias[i]
|
||||
cwq[i] = amplitude[i]*sin(t[si:]*f_LO*1e-9*(2*np.pi+(phase[i]+90)*np.pi/180))+bias[i]
|
||||
|
||||
iq[i] = channel[i]*cwi[i]
|
||||
q[i] = channel[i]*cwq[i]
|
||||
|
||||
return iq,q
|
||||
'''
|
||||
def sinW(self,sig,freq,ti,tf):
|
||||
# sig: signal array
|
||||
# freq
|
||||
# ti, tf: initial and end time
|
||||
si = int(ti * self.samp_freq)
|
||||
nperiods = freq * (tf - ti)
|
||||
n = int(round(max(2, int(nperiods)) / nperiods * (tf-ti) * self.samp_freq))
|
||||
self.nperiods = n
|
||||
t = np.arange(si, len(sig)) / self.samp_freq
|
||||
t = t[:n]
|
||||
self.pulselen = n / self.samp_freq
|
||||
sig = sig[si:si+n]
|
||||
a = 2*np.sum(sig*np.cos(2*np.pi*freq*t))/len(sig)
|
||||
b = 2*np.sum(sig*np.sin(2*np.pi*freq*t))/len(sig)
|
||||
return a, b
|
||||
|
||||
def mix(self, sigin, sigout, freq, ti, tf):
|
||||
# sigin, sigout: signal array, incomping, output
|
||||
# freq
|
||||
# ti, tf: initial and end time if sigin
|
||||
a, b = self.sinW(sigin, freq, ti, tf)
|
||||
phase = arctan2(a,b) * 180 / np.pi
|
||||
amp = sqrt(a**2 + b**2)
|
||||
a, b = a/amp, b/amp
|
||||
#si = int(ti * self.samp_freq)
|
||||
t = np.arange(len(sigout)) / self.samp_freq
|
||||
wave1 = sigout * (a * cos(2*np.pi*freq*t) + b * sin(2*np.pi*freq*t))
|
||||
wave2 = sigout * (a * sin(2*np.pi*freq*t) - b * cos(2*np.pi*freq*t))
|
||||
return wave1, wave2
|
||||
|
||||
def averageiq(self, data, freq, ti, tf):
|
||||
'''Average over records'''
|
||||
iorq = np.array([self.mix(data[0][i], data[1][i], freq, ti, tf) for i in range(self.number_of_records)])
|
||||
# iorq = np.array([self.mix(data[0][:], data[1][:], freq, ti, tf)])
|
||||
return iorq.sum(axis=0) / self.number_of_records
|
||||
|
||||
def filtro(self, iorq, cutoff):
|
||||
b, a = butter_lowpass(cutoff, self.samp_freq*1e9)
|
||||
|
||||
#ifi = np.array(signal.filtfilt(b,a,iorq[0]))
|
||||
#qf = np.array(signal.filtfilt(b,a,iorq[1]))
|
||||
iqf = [signal.filtfilt(b,a,iorq[i]) for i in np.arange(len(iorq))]
|
||||
|
||||
return iqf
|
||||
|
||||
def box(self, iorq, ti, tf):
|
||||
si = int(self.samp_freq * ti)
|
||||
sf = int(self.samp_freq * tf)
|
||||
bxa = [sum(iorq[i][si:sf])/(sf-si) for i in np.arange(len(iorq))]
|
||||
return bxa
|
||||
|
||||
def gates_and_curves(self, data, freq, pulse, roi):
|
||||
"""return iq values of rois and prepare plottable curves for iq"""
|
||||
times = []
|
||||
times.append(('aviq', time.time()))
|
||||
iq = self.averageiq(data,freq*1e-9,*pulse)
|
||||
times.append(('filtro', time.time()))
|
||||
iqf = self.filtro(iq,self.bw_cutoff)
|
||||
m = len(iqf[0]) // self.ndecimate
|
||||
times.append(('iqdec', time.time()))
|
||||
iqd = np.average(np.resize(iqf, (2, m, self.ndecimate)), axis=2)
|
||||
t_axis = np.arange(m) * self.ndecimate / self.samp_freq
|
||||
pulsig = np.abs(data[0][0])
|
||||
times.append(('pulsig', time.time()))
|
||||
pulsig = np.average(np.resize(pulsig, (m, self.ndecimate)), axis=1)
|
||||
self.curves = (t_axis, iqd[0], iqd[1], pulsig)
|
||||
#print(times)
|
||||
return [self.box(iqf,*r) for r in roi]
|
||||
|
264
frappy_psi/attocube.py
Normal file
264
frappy_psi/attocube.py
Normal file
@ -0,0 +1,264 @@
|
||||
# *****************************************************************************
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Module authors:
|
||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||
# *****************************************************************************
|
||||
|
||||
import sys
|
||||
import time
|
||||
from frappy.core import Drivable, Parameter, Command, Property, ERROR, WARN, BUSY, IDLE, Done, nopoll
|
||||
from frappy.features import HasTargetLimits, HasSimpleOffset
|
||||
from frappy.datatypes import IntRange, FloatRange, StringType, BoolType
|
||||
from frappy.errors import ConfigError, BadValueError
|
||||
sys.path.append('/home/l_samenv/Documents/anc350/Linux64/userlib/lib')
|
||||
from PyANC350v4 import Positioner
|
||||
|
||||
|
||||
DIRECTION_NAME = {1: 'forward', -1: 'backward'}
|
||||
|
||||
|
||||
class FreezeStatus:
|
||||
"""freeze status for some time
|
||||
|
||||
hardware quite often does not treat status correctly: on a target change it
|
||||
may take some time to return the 'busy' status correctly.
|
||||
|
||||
in classes with this mixin, within :meth:`write_target` call
|
||||
|
||||
self.freeze_status(0.5, BUSY, 'changed target')
|
||||
|
||||
a wrapper around read_status will take care that the status will be the given value,
|
||||
for at least the given delay. This does NOT cover the case when self.status is set
|
||||
directly from an other method.
|
||||
"""
|
||||
__freeze_status_until = 0
|
||||
|
||||
def __init_subclass__(cls):
|
||||
def wrapped(self, inner=cls.read_status):
|
||||
if time.time() < self.__freeze_status_until:
|
||||
return Done
|
||||
return inner(self)
|
||||
|
||||
cls.read_status = wrapped
|
||||
super().__init_subclass__()
|
||||
|
||||
def freeze_status(self, delay, code=BUSY, text='changed target'):
|
||||
"""freezze status to the given value for the given delay"""
|
||||
self.__freeze_status_until = time.time() + delay
|
||||
self.status = code, text
|
||||
|
||||
|
||||
class Axis(HasTargetLimits, FreezeStatus, Drivable):
|
||||
axis = Property('axis number', IntRange(0, 2), 0)
|
||||
value = Parameter('axis position', FloatRange(unit='deg'))
|
||||
frequency = Parameter('frequency', FloatRange(1, unit='Hz'), readonly=False)
|
||||
amplitude = Parameter('amplitude', FloatRange(0, unit='V'), readonly=False)
|
||||
gear = Parameter('gear factor', FloatRange(), readonly=False, default=1, initwrite=True)
|
||||
tolerance = Parameter('positioning tolerance', FloatRange(0, unit='$'), readonly=False, default=0.01)
|
||||
output = Parameter('enable output', BoolType(), readonly=False)
|
||||
info = Parameter('axis info', StringType())
|
||||
statusbits = Parameter('status bits', StringType())
|
||||
|
||||
_hw = Positioner()
|
||||
_scale = 1 # scale for custom units
|
||||
_move_steps = 0 # number of steps to move (used by move command)
|
||||
SCALES = {'deg': 1, 'm': 1, 'mm': 1000, 'um': 1000000, 'µm': 1000000}
|
||||
_direction = 1 # move direction
|
||||
_idle_status = IDLE, ''
|
||||
_error_state = '' # empty string: no error
|
||||
_history = None
|
||||
_check_sensor = False
|
||||
_try_count = 0
|
||||
|
||||
def __init__(self, name, logger, opts, srv):
|
||||
unit = opts.pop('unit', 'deg')
|
||||
opts['value.unit'] = unit
|
||||
try:
|
||||
self._scale = self.SCALES[unit] * opts.get('gear', 1)
|
||||
except KeyError as e:
|
||||
raise ConfigError('unsupported unit: %s' % unit)
|
||||
super().__init__(name, logger, opts, srv)
|
||||
|
||||
def write_gear(self, value):
|
||||
self._scale = self.SCALES[self.parameters['value'].datatype.unit] * self.gear
|
||||
return value
|
||||
|
||||
def startModule(self, start_events):
|
||||
super().startModule(start_events)
|
||||
start_events.queue(self.read_info)
|
||||
|
||||
def check_value(self, value):
|
||||
"""check if value allows moving in current direction"""
|
||||
if self._direction > 0:
|
||||
if value > self.target_limits[1]:
|
||||
raise BadValueError('above upper limit')
|
||||
elif value < self.target_limits[0]:
|
||||
raise BadValueError('below lower limit')
|
||||
|
||||
def read_value(self):
|
||||
pos = self._hw.getPosition(self.axis) * self._scale
|
||||
if self.isBusy():
|
||||
try:
|
||||
self.check_value(pos)
|
||||
except BadValueError as e:
|
||||
self._stop()
|
||||
self._idle_status = ERROR, str(e)
|
||||
return pos
|
||||
|
||||
def read_frequency(self):
|
||||
return self._hw.getFrequency(self.axis)
|
||||
|
||||
def write_frequency(self, value):
|
||||
self._hw.setFrequency(self.axis, value)
|
||||
return self._hw.getFrequency(self.axis)
|
||||
|
||||
def read_amplitude(self):
|
||||
return self._hw.getAmplitude(self.axis)
|
||||
|
||||
def write_amplitude(self, value):
|
||||
self._hw.setAmplitude(self.axis, value)
|
||||
return self._hw.getAmplitude(self.axis)
|
||||
|
||||
def write_tolerance(self, value):
|
||||
self._hw.setTargetRange(self.axis, value / self._scale)
|
||||
return value
|
||||
|
||||
def write_output(self, value):
|
||||
self._hw.setAxisOutput(self.axis, enable=value, autoDisable=0)
|
||||
return value
|
||||
|
||||
def read_status(self):
|
||||
statusbits = self._hw.getAxisStatus(self.axis)
|
||||
sensor, self.output, moving, attarget, eot_fwd, eot_bwd, sensor_error = statusbits
|
||||
self.statusbits = ''.join((k for k, v in zip('SOMTFBE', statusbits) if v))
|
||||
if self._move_steps:
|
||||
if not (eot_fwd or eot_bwd):
|
||||
return BUSY, 'moving by steps'
|
||||
if not sensor:
|
||||
self._error_state = 'no sensor connected'
|
||||
elif sensor_error:
|
||||
self._error_state = 'sensor error'
|
||||
elif eot_fwd:
|
||||
self._error_state = 'end of travel forward'
|
||||
elif eot_bwd:
|
||||
self._error_state = 'end of travel backward'
|
||||
else:
|
||||
if self._error_state and not DIRECTION_NAME[self._direction] in self._error_state:
|
||||
self._error_state = ''
|
||||
status_text = 'moving' if self._try_count == 0 else 'moving (retry %d)' % self._try_count
|
||||
if moving and self._history is not None: # history None: moving by steps
|
||||
self._history.append(self.value)
|
||||
if len(self._history) < 5:
|
||||
return BUSY, status_text
|
||||
beg = self._history.pop(0)
|
||||
if abs(beg - self.target) < self.tolerance:
|
||||
# reset normal tolerance
|
||||
self._stop()
|
||||
self._idle_status = IDLE, 'in tolerance'
|
||||
return self._idle_status
|
||||
# self._hw.setTargetRange(self.axis, self.tolerance / self._scale)
|
||||
if (self.value - beg) * self._direction > 0:
|
||||
return BUSY, status_text
|
||||
self._try_count += 1
|
||||
if self._try_count < 10:
|
||||
self.log.warn('no progress retry %d', self._try_count)
|
||||
return BUSY, status_text
|
||||
self._idle_status = WARN, 'no progress'
|
||||
if self._error_state:
|
||||
self._try_count += 1
|
||||
if self._try_count < 10 and self._history is not None:
|
||||
self.log.warn('end of travel retry %d', self._try_count)
|
||||
self.write_target(self.target)
|
||||
return Done
|
||||
self._idle_status = WARN, self._error_state
|
||||
if self.status[0] != IDLE:
|
||||
self._stop()
|
||||
return self._idle_status
|
||||
|
||||
def write_target(self, value):
|
||||
if value == self.read_value():
|
||||
return value
|
||||
self.check_limits(value)
|
||||
self._try_count = 0
|
||||
self._direction = 1 if value > self.value else -1
|
||||
# if self._error_state and DIRECTION_NAME[-self._direction] not in self._error_state:
|
||||
# raise BadValueError('can not move (%s)' % self._error_state)
|
||||
self._move_steps = 0
|
||||
self.write_output(1)
|
||||
# try first with 50 % of tolerance
|
||||
self._hw.setTargetRange(self.axis, self.tolerance * 0.5 / self._scale)
|
||||
for itry in range(5):
|
||||
try:
|
||||
self._hw.setTargetPosition(self.axis, value / self._scale)
|
||||
self._hw.startAutoMove(self.axis, enable=1, relative=0)
|
||||
except Exception as e:
|
||||
if itry == 4:
|
||||
raise
|
||||
self.log.warn('%r', e)
|
||||
self._history = [self.value]
|
||||
self._idle_status = IDLE, ''
|
||||
self.freeze_status(1, BUSY, 'changed target')
|
||||
self.setFastPoll(True, 1)
|
||||
return value
|
||||
|
||||
def doPoll(self):
|
||||
if self._move_steps == 0:
|
||||
super().doPoll()
|
||||
return
|
||||
self._hw.startSingleStep(self.axis, self._direction < 0)
|
||||
self._move_steps -= self._direction
|
||||
if self._move_steps % int(self.frequency) == 0: # poll value and status every second
|
||||
super().doPoll()
|
||||
|
||||
@nopoll
|
||||
def read_info(self):
|
||||
"""read info from controller"""
|
||||
cap = self._hw.measureCapacitance(self.axis) * 1e9
|
||||
axistype = ['linear', 'gonio', 'rotator'][self._hw.getActuatorType(self.axis)]
|
||||
return '%s %s %.3gnF' % (self._hw.getActuatorName(self.axis), axistype, cap)
|
||||
|
||||
def _stop(self):
|
||||
self._move_steps = 0
|
||||
self._history = None
|
||||
for _ in range(5):
|
||||
try:
|
||||
self._hw.startAutoMove(self.axis, enable=0, relative=0)
|
||||
break
|
||||
except Exception as e:
|
||||
if itry == 4:
|
||||
raise
|
||||
self.log.warn('%r', e)
|
||||
self._hw.setTargetRange(self.axis, self.tolerance / self._scale)
|
||||
self.setFastPoll(False)
|
||||
|
||||
@Command()
|
||||
def stop(self):
|
||||
self._idle_status = IDLE, 'stopped' if self.isBusy() else ''
|
||||
self._stop()
|
||||
self.status = self._idle_status
|
||||
|
||||
@Command(IntRange())
|
||||
def move(self, value):
|
||||
"""relative move by number of steps"""
|
||||
self._direction = 1 if value > 0 else -1
|
||||
self.check_value(self.value)
|
||||
self._history = None
|
||||
if DIRECTION_NAME[self._direction] in self._error_state:
|
||||
raise BadValueError('can not move (%s)' % self._error_state)
|
||||
self._move_steps = value
|
||||
self._idle_status = IDLE, ''
|
||||
self.read_status()
|
||||
self.setFastPoll(True, 1/self.frequency)
|
584
frappy_psi/cryoltd.py
Normal file
584
frappy_psi/cryoltd.py
Normal file
@ -0,0 +1,584 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# *****************************************************************************
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Module authors:
|
||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||
# *****************************************************************************
|
||||
|
||||
"""flame magnet using cryogenic limited software suite
|
||||
|
||||
Remarks: for reading, we use the GetAll command, which is quite fast
|
||||
(3 ms response time). However, as in many such sloppy programmed software
|
||||
the timing is badly handled, as after changing parameters, the readback values
|
||||
are not yet up to date. We use the array block_until for this purpose: any value
|
||||
changed from the client is fixed for at least 10 seconds.
|
||||
"""
|
||||
import re
|
||||
import time
|
||||
from frappy.core import HasIO, StringIO, Readable, Drivable, Parameter, Command, \
|
||||
Module, Property, Attached, Enum, IDLE, BUSY, ERROR, Done
|
||||
from frappy.errors import ConfigError, BadValueError, HardwareError
|
||||
from frappy.datatypes import FloatRange, StringType, EnumType, StructOf
|
||||
from frappy.states import HasStates, status_code, Retry
|
||||
from frappy.features import HasTargetLimits
|
||||
import frappy_psi.magfield as magfield
|
||||
|
||||
# floating point value followed with unit
|
||||
VALUE_UNIT = re.compile(r'([-0-9.E]*\d|inf)([A-Za-z/%]*)$')
|
||||
|
||||
|
||||
def as_float(value):
|
||||
"""converts string (with unit) to float"""
|
||||
return float(VALUE_UNIT.match(value).group(1))
|
||||
|
||||
|
||||
BOOL_MAP = {'TRUE': True, 'FALSE': False}
|
||||
|
||||
|
||||
def as_bool(value):
|
||||
return BOOL_MAP[value]
|
||||
|
||||
|
||||
def as_string(value):
|
||||
return value
|
||||
|
||||
|
||||
class Mapped:
|
||||
def __init__(self, **kwds):
|
||||
self.mapping = kwds
|
||||
self.mapping.update({v: k for k, v in kwds.items()})
|
||||
|
||||
def __call__(self, value):
|
||||
return self.mapping[value]
|
||||
|
||||
|
||||
class IO(StringIO):
|
||||
identification = [('Get:Remote:Remote_Ready', 'Remote_Ready RECEIVED: TRUE')]
|
||||
end_of_line = (b'\n', b'\r\n')
|
||||
timeout = 15
|
||||
|
||||
|
||||
class Main(HasIO, Module):
|
||||
pollinterval = Parameter('main poll interval', FloatRange(0.001), default=1)
|
||||
export = False
|
||||
params_map = None
|
||||
triggers = None
|
||||
initpolls = None
|
||||
ioClass = IO
|
||||
|
||||
def register_module(self, obj, **params):
|
||||
"""register a channel
|
||||
|
||||
:param obj: the remote object
|
||||
:param **params:
|
||||
pname=<Key in GetAll> or pname=(<Key in Getall>, convert function)
|
||||
the convert function is as_float by default
|
||||
"""
|
||||
if self.params_map is None:
|
||||
self.params_map = {}
|
||||
self.triggers = set()
|
||||
obj.block_until = {}
|
||||
if hasattr(obj, 'trigger_update'):
|
||||
self.triggers.add(obj.trigger_update)
|
||||
for pname, value in params.items():
|
||||
if isinstance(value, str):
|
||||
key, cvt = value, as_float
|
||||
else:
|
||||
key, cvt = value
|
||||
self.params_map[key.replace('<CH>', obj.channel)] = obj, pname, cvt
|
||||
|
||||
def doPoll(self):
|
||||
# format of reply:
|
||||
# "key1:value1;key2:value2;"
|
||||
reply = self.communicate('GetAll').split('RECEIVED ', 1)[1].split(';')
|
||||
missing = None, None, None
|
||||
for line in reply:
|
||||
try:
|
||||
key, value = line.split(':', 1)
|
||||
except ValueError: # missing ':'
|
||||
if line:
|
||||
pass
|
||||
# ignore multiline values
|
||||
# if needed, we may collect here and treat with a special key
|
||||
continue
|
||||
obj, pname, cvt = self.params_map.get(key, missing)
|
||||
if obj:
|
||||
if not hasattr(obj, pname):
|
||||
raise ConfigError('%s has no attribute %s' % (obj.name, pname))
|
||||
if time.time() > obj.block_until.get(pname, 0):
|
||||
setattr(obj, pname, cvt(value))
|
||||
for trigger in self.triggers: # do trigger after updates
|
||||
trigger()
|
||||
|
||||
def communicate(self, cmd):
|
||||
return self.io.communicate(cmd)
|
||||
|
||||
|
||||
class Channel:
|
||||
main = Attached(Main, default='main')
|
||||
channel = Property('channel name', StringType())
|
||||
pollinterval = Parameter(export=False)
|
||||
block_until = None
|
||||
|
||||
def sendcmd(self, cmd):
|
||||
cmd = cmd.replace('<CH>', self.channel)
|
||||
reply = self.main.communicate(cmd)
|
||||
if not reply.startswith(cmd):
|
||||
self.log.warn('reply %r does not match command %r', reply, cmd)
|
||||
|
||||
def block(self, pname, value=None, delay=10):
|
||||
self.block_until[pname] = time.time() + delay
|
||||
if value is not None:
|
||||
setattr(self, pname, value)
|
||||
|
||||
def doPoll(self):
|
||||
"""poll is done by main module"""
|
||||
|
||||
|
||||
class Temperature(Channel, Readable):
|
||||
channel = Property('channel name as in reply to GetAll', StringType())
|
||||
value = Parameter('T sensor value', FloatRange(0, unit='K'))
|
||||
description = '' # by default take description from channel name
|
||||
|
||||
KEYS = {
|
||||
'1st Stage A',
|
||||
'2nd Stage A',
|
||||
'1st Stage B',
|
||||
'2nd Stage B',
|
||||
'Inner Magnet A (Top)',
|
||||
'Inner Magnet A (Bottom)',
|
||||
'Z Shim Former',
|
||||
'XY Shim Former',
|
||||
'XY Vector Former',
|
||||
'Radiation Shield',
|
||||
'Persistent Joints',
|
||||
'Outer Magnet A',
|
||||
'Inner Magnet B (Top)',
|
||||
'Inner Magnet B (Bottom)',
|
||||
'Z Shim Former B',
|
||||
'Outer Magnet B',
|
||||
'Bore Radiation Shield',
|
||||
'XYZ Shim Plate',
|
||||
'Z Shim Switch',
|
||||
'Main Coil Switch',
|
||||
}
|
||||
|
||||
def initModule(self):
|
||||
super().initModule()
|
||||
if not self.description:
|
||||
self.description = self.channel
|
||||
if self.channel not in self.KEYS:
|
||||
raise ConfigError('channel (%s) must be one of %r' % (self.channel, self.KEYS))
|
||||
self.main.register_module(self, value=self.channel)
|
||||
|
||||
|
||||
CsMode = Enum(
|
||||
PERSISTENT=1,
|
||||
SEMIPERSISTENT=2,
|
||||
DRIVEN=0,
|
||||
)
|
||||
|
||||
PersistencyMode = Enum(
|
||||
DISABLED=0,
|
||||
PERSISTENT=30,
|
||||
SEMIPERSISTENT=31,
|
||||
DRIVEN=50,
|
||||
)
|
||||
|
||||
|
||||
class BaseMagfield(HasStates, HasTargetLimits, Channel):
|
||||
_status_text = ''
|
||||
_ready_text = ''
|
||||
_error_text = ''
|
||||
_last_error = ''
|
||||
_rate_units = ''
|
||||
hw_units = Property('hardware units: A or T', EnumType(A=0, T=1))
|
||||
A_to_G = Property('A_to_G = "Gauss Value / Amps Value"', FloatRange(0))
|
||||
tolerance = Parameter('tolerance', FloatRange(0), readonly=False, default=1)
|
||||
|
||||
def earlyInit(self):
|
||||
super().earlyInit()
|
||||
dt = self.parameters['target'].datatype
|
||||
if dt.min < 1e-99: # make limits symmetric
|
||||
dt.min = - dt.max
|
||||
min_, max_ = self.target_limits
|
||||
self.target_limits = [max(min_, dt.min), min(max_, dt.max)]
|
||||
dt = self.parameters['ramp'].datatype
|
||||
if self.ramp == 0: # unconfigured: take max.
|
||||
self.ramp = dt.max
|
||||
if not isinstance(self, magfield.Magfield):
|
||||
# add unneeded attributes, as the appear in GetAll
|
||||
self.switch_heater = None
|
||||
self.current = None
|
||||
|
||||
def doPoll(self):
|
||||
super().doPoll() # does not call cycle_machine
|
||||
self.cycle_machine()
|
||||
|
||||
def to_gauss(self, value):
|
||||
value, unit = VALUE_UNIT.match(value).groups()
|
||||
if unit == 'T':
|
||||
return float(value) * 10000
|
||||
if unit == 'A':
|
||||
return float(value) * self.A_to_G
|
||||
raise HardwareError('received unknown unit: %s' % unit)
|
||||
|
||||
def to_gauss_min(self, value):
|
||||
value, unit = VALUE_UNIT.match(value).groups()
|
||||
if unit == 'A/s':
|
||||
return float(value) * self.A_to_G * 60.0
|
||||
if unit == 'T/s':
|
||||
return float(value) * 10000 * 60
|
||||
if unit == 'T/min':
|
||||
return float(value) * 10000
|
||||
if unit == 'T/h':
|
||||
return float(value) * 10000 / 60.0
|
||||
raise HardwareError('received unknown unit: %s' % unit)
|
||||
|
||||
def initModule(self):
|
||||
super().initModule()
|
||||
self.main.register_module(
|
||||
self,
|
||||
value=('<CH>_Field', self.to_gauss),
|
||||
_status_text=('<CH>_Status', str),
|
||||
_ready_text=('<CH>_Ready', str),
|
||||
_error_text=('<CH>_Error', self.cvt_error),
|
||||
_rate_units=('<CH>_Rate Units', str),
|
||||
current=('<CH>_PSU Output', self.to_gauss),
|
||||
voltage='<CH>_Voltage',
|
||||
workingramp=('<CH>_Ramp Rate', self.to_gauss_min),
|
||||
setpoint=('<CH>_Setpoint', self.to_gauss),
|
||||
switch_heater=('<CH>_Heater', self.cvt_switch_heater),
|
||||
cs_mode=('<CH>_Persistent Mode', self.cvt_cs_mode),
|
||||
approach_mode=('<CH>_Approach', self.cvt_approach_mode),
|
||||
)
|
||||
|
||||
def cvt_error(self, text):
|
||||
if text != self._last_error:
|
||||
self._last_error = text
|
||||
self.log.error(text)
|
||||
return text
|
||||
return self._error_text
|
||||
|
||||
def trigger_update(self):
|
||||
# called after treating result of GetAll message
|
||||
if self._error_text:
|
||||
if self.status[0] == ERROR:
|
||||
errtxt = self._error_text
|
||||
else:
|
||||
errtxt = '%s while %s' % (self._error_text, self.status[1])
|
||||
# self.stop_machine((ERROR, errtxt))
|
||||
|
||||
value = Parameter('magnetic field in the coil', FloatRange(unit='G'))
|
||||
|
||||
setpoint = Parameter('setpoint', FloatRange(unit='G'), default=0)
|
||||
ramp = Parameter()
|
||||
target = Parameter()
|
||||
|
||||
def write_ramp(self, ramp):
|
||||
if self._rate_units != 'A/s':
|
||||
self.sendcmd('Set:<CH>:ChangeRateUnits A/s')
|
||||
self.sendcmd('Set:<CH>:SetRate %g' % (ramp / self.A_to_G / 60.0))
|
||||
return ramp
|
||||
|
||||
def write_target(self, target):
|
||||
self.reset_error()
|
||||
super().write_target(target)
|
||||
return Done
|
||||
|
||||
def start_sweep(self, target):
|
||||
if self.approach_mode == self.approach_mode.OVERSHOOT:
|
||||
o = self.overshoot['o']
|
||||
if (target - self.value) * o < 0:
|
||||
self.write_overshoot(dict(self.overshoot, o=-o))
|
||||
self.write_ramp(self.ramp)
|
||||
if self.hw_units == 'A':
|
||||
self.sendcmd('Set:<CH>:Sweep %gA' % (target / self.A_to_G))
|
||||
else:
|
||||
self.sendcmd('Set:<CH>:Sweep %gT' % (target / 10000))
|
||||
self.block('_ready_text', 'FALSE')
|
||||
|
||||
def start_field_change(self, sm):
|
||||
if self._ready_text == 'FALSE':
|
||||
self.sendcmd('Set:<CH>:Abort')
|
||||
return self.wait_ready
|
||||
return super().start_field_change
|
||||
|
||||
def wait_ready(self, sm):
|
||||
if self._ready_text == 'FALSE':
|
||||
return Retry
|
||||
return super().start_field_change
|
||||
|
||||
def start_ramp_to_target(self, sm):
|
||||
self.start_sweep(sm.target)
|
||||
return self.ramp_to_target # -> stabilize_field
|
||||
|
||||
def stabilize_field(self, sm):
|
||||
if self._ready_text == 'FALSE':
|
||||
# wait for overshoot/degauss/cycle
|
||||
sm.stabilize_start = sm.now
|
||||
return Retry
|
||||
if sm.now - sm.stabilize_start < self.wait_stable_field:
|
||||
return Retry
|
||||
return self.end_stablilize
|
||||
|
||||
def end_stablilize(self, sm):
|
||||
return self.final_status()
|
||||
|
||||
cs_mode = Parameter('persistent mode', EnumType(CsMode), readonly=False, default=0)
|
||||
|
||||
@staticmethod
|
||||
def cvt_cs_mode(text):
|
||||
text = text.lower()
|
||||
if 'off' in text:
|
||||
if '0' in text:
|
||||
return CsMode.PERSISTENT
|
||||
return CsMode.SEMIPERSISTENT
|
||||
return CsMode.DRIVEN
|
||||
|
||||
def write_cs_mode(self, value):
|
||||
self.sendcmd('Set:<CH>:SetPM %d' % int(value))
|
||||
self.block('cs_mode')
|
||||
return value
|
||||
|
||||
ApproachMode = Enum(
|
||||
DIRECT=0,
|
||||
OVERSHOOT=1,
|
||||
CYCLING=2,
|
||||
DEGAUSSING=3,
|
||||
)
|
||||
approach_mode = Parameter('approach mode', EnumType(ApproachMode), readonly=False,
|
||||
group='approach_settings', default=0)
|
||||
|
||||
def write_approach_mode(self, value):
|
||||
self.sendcmd('Set:<CH>:SetApproach %d' % value)
|
||||
self.block('approach_mode')
|
||||
return value
|
||||
|
||||
@classmethod
|
||||
def cvt_approach_mode(cls, text):
|
||||
return cls.ApproachMode(text.upper())
|
||||
|
||||
overshoot = Parameter('overshoot [%] and hold time [s]',
|
||||
StructOf(o=FloatRange(-100, 100, unit='%'), t=FloatRange(0, unit='s')),
|
||||
readonly=False, default=dict(o=0, t=0),
|
||||
group='approach_settings')
|
||||
|
||||
def write_overshoot(self, value):
|
||||
self.sendcmd('Set:<CH>:SetOvershoot %g,%g' % (value['o'], value['t']))
|
||||
return value
|
||||
|
||||
cycle = Parameter('start value, damping factor, final value, hold time',
|
||||
StructOf(s=FloatRange(-100, 100, unit='%'),
|
||||
d=FloatRange(0, 100, unit='%'),
|
||||
a=FloatRange(0, 100, unit='G'),
|
||||
t=FloatRange(0, unit='s')),
|
||||
readonly=False, default=dict(s=0, d=0, a=0, t=0),
|
||||
group='approach_settings')
|
||||
|
||||
def write_cycle(self, value):
|
||||
self.sendcmd('Set:<CH>:SetCycling %g,%g,%g,%g' %
|
||||
(value['s'], value['d'], value['a'] * 1e-4, value['t']))
|
||||
return value
|
||||
|
||||
degauss = Parameter('start value [G], damping factor [%], accuracy [G], hold time [s]',
|
||||
StructOf(s=FloatRange(-10000, 10000, unit='G'),
|
||||
d=FloatRange(0, 100, unit='%'),
|
||||
f=FloatRange(0, 10000, unit='G'),
|
||||
t=FloatRange(0, unit='s')),
|
||||
readonly=False, default=dict(s=0, d=0, f=0, t=0),
|
||||
group='approach_settings')
|
||||
|
||||
def write_degauss(self, value):
|
||||
self.sendcmd('Set:<CH>:SetDegaussing %g,%g,%g,%g' %
|
||||
(value['s'] * 1e-4, value['d'], value['f'] * 1e-4, value['t']))
|
||||
return value
|
||||
|
||||
voltage = Parameter(
|
||||
'voltage over leads', FloatRange(unit='V'))
|
||||
|
||||
@staticmethod
|
||||
def cvt_switch_heater(text):
|
||||
return 'ON' in text
|
||||
|
||||
@Command()
|
||||
def stop(self):
|
||||
self.sendcmd('Set:<CH>:Abort')
|
||||
|
||||
@Command()
|
||||
def reset_quench(self):
|
||||
"""reset quench condition"""
|
||||
self.sendcmd('Set:<CH>:ResetQuench')
|
||||
|
||||
@Command()
|
||||
def reset_error(self):
|
||||
"""reset error"""
|
||||
self._error_text = ''
|
||||
|
||||
|
||||
class MainField(BaseMagfield, magfield.Magfield):
|
||||
checked_modules = None
|
||||
|
||||
def earlyInit(self):
|
||||
super().earlyInit()
|
||||
self.checked_modules = []
|
||||
|
||||
def check_limits(self, value):
|
||||
super().check_limits(value)
|
||||
self.check_combined(None, 0, value)
|
||||
|
||||
# TODO: turn into a property
|
||||
constraint = Parameter('product check', FloatRange(unit='G^2'), default=80000)
|
||||
|
||||
def check_combined(self, obj, value, main_target):
|
||||
sumvalue2 = sum(((value ** 2 if o == obj else o.value ** 2)
|
||||
for o in self.checked_modules))
|
||||
if sumvalue2 * max(self.value ** 2, main_target ** 2) > self.constraint ** 2:
|
||||
raise BadValueError('outside constraint (B * Bxyz > %g G^2' % self.constraint)
|
||||
|
||||
def check_limits(self, value):
|
||||
super().check_limits(value)
|
||||
self.check_combined(None, 0, value)
|
||||
|
||||
mode = Parameter(datatype=EnumType(PersistencyMode))
|
||||
|
||||
def write_mode(self, mode):
|
||||
self.reset_error()
|
||||
super().write_mode(mode) # updates mode
|
||||
return Done
|
||||
|
||||
@status_code('PREPARING')
|
||||
def start_ramp_to_field(self, sm):
|
||||
self.start_sweep(self.value)
|
||||
return self.ramp_to_field # -> stabilize_current -> start_switch_on
|
||||
|
||||
@status_code('PREPARING')
|
||||
def start_switch_on(self, sm):
|
||||
self.write_cs_mode(CsMode.DRIVEN)
|
||||
# self.block('switch_heater', 1, 60)
|
||||
self.start_sweep(self.value)
|
||||
self.block('_ready_text', 'FALSE')
|
||||
return self.wait_for_switch_on # -> start_ramp_to_target -> ramp_to_target -> stabilize_field
|
||||
|
||||
@status_code('PREPARING')
|
||||
def wait_for_switch_on(self, sm):
|
||||
if self._ready_text == 'FALSE':
|
||||
return Retry
|
||||
self.last_target(sm.target)
|
||||
return self.start_ramp_to_target
|
||||
|
||||
def end_stablilize(self, sm):
|
||||
return self.start_switch_off
|
||||
|
||||
@status_code('FINALIZING')
|
||||
def start_switch_off(self, sm):
|
||||
if self.mode == PersistencyMode.DRIVEN:
|
||||
return self.final_status(IDLE, 'driven')
|
||||
if self.mode == PersistencyMode.SEMIPERSISTENT:
|
||||
self.write_cs_mode(CsMode.SEMIPERSISTENT)
|
||||
else: # PERSISTENT or DISABLED
|
||||
self.write_cs_mode(CsMode.PERSISTENT)
|
||||
self.start_sweep(sm.target)
|
||||
self.block('_ready_text', 'FALSE')
|
||||
self.block('switch_heater', 1)
|
||||
return self.wait_for_switch_off # -> start_ramp_to_zero
|
||||
|
||||
@status_code('PREPARING')
|
||||
def wait_for_switch_off(self, sm):
|
||||
if self.switch_heater:
|
||||
return Retry
|
||||
self.last_target(sm.target)
|
||||
if self.mode == PersistencyMode.SEMIPERSISTENT:
|
||||
return self.final_status(IDLE, 'semipersistent')
|
||||
return self.ramp_to_zero
|
||||
|
||||
@status_code('FINALIZING')
|
||||
def ramp_to_zero(self, sm):
|
||||
if self._ready_text == 'FALSE':
|
||||
return Retry
|
||||
if self.mode == PersistencyMode.DISABLED:
|
||||
return self.final_status(PersistencyMode.DISABLED, 'disabled')
|
||||
return self.final_status(IDLE, 'persistent')
|
||||
|
||||
|
||||
class ComponentField(BaseMagfield, magfield.SimpleMagfield):
|
||||
check_against = Attached(MainField)
|
||||
# status = Parameter(datatype=EnumType(Drivable.Status, RAMPING=370, STABILIZING=380, FINALIZING=390))
|
||||
|
||||
def initModule(self):
|
||||
super().initModule()
|
||||
self.check_against.checked_modules.append(self)
|
||||
|
||||
def check_limits(self, value):
|
||||
super().check_limits(value)
|
||||
self.check_against.check_combined(self, value, 0)
|
||||
|
||||
|
||||
class Compressor(Channel, Drivable):
|
||||
def initModule(self):
|
||||
super().initModule()
|
||||
self.main.register_module(
|
||||
self,
|
||||
value=('Compressor<CH>_Status', self.cvt_value),
|
||||
_ready_text=('Compressor<CH>_Ready', str),
|
||||
_error_text=('Compressor<CH>_Error', str),
|
||||
run_time='Compressor<CH>_RunTime',
|
||||
)
|
||||
# TODO: what is Compressor_Error? (without A or B)
|
||||
|
||||
value = Parameter('compressor switch', EnumType(OFF=0, ON=1))
|
||||
run_time = Parameter('run time', FloatRange(0, unit='h'))
|
||||
|
||||
_status_text = ''
|
||||
_ready_text = ''
|
||||
_error_text = ''
|
||||
|
||||
def cvt_value(self, text):
|
||||
self._status_text = text
|
||||
value = text == 'Running'
|
||||
if time.time() > self.block_until.get('target', 0):
|
||||
self.target = value
|
||||
return value
|
||||
|
||||
def read_value(self):
|
||||
return self._status_text == 'Running'
|
||||
|
||||
def read_status(self):
|
||||
if self.target != self.value:
|
||||
return BUSY, 'switching %s' % self.target.name
|
||||
# TODO: find out possible status texts
|
||||
if self._ready_text == 'TRUE':
|
||||
return IDLE, 'ready'
|
||||
if self._error_text:
|
||||
return ERROR, self._error_text
|
||||
return IDLE, self._status_text
|
||||
|
||||
target = Parameter('compressor switch', EnumType(OFF=0, ON=1))
|
||||
|
||||
def write_target(self, value):
|
||||
if value:
|
||||
self.sendcmd('Set:Compressor:Start <CH>')
|
||||
else:
|
||||
self.sendcmd('Set:Compressor:Stop <CH>')
|
||||
self.block('target')
|
||||
self.read_status()
|
||||
return value
|
||||
|
||||
@Command()
|
||||
def reset_error(self):
|
||||
"""reset error"""
|
||||
self.sendcmd('Set:Compressor:Reset <CH>')
|
||||
self._error_text = ''
|
144
frappy_psi/cryotel.py
Normal file
144
frappy_psi/cryotel.py
Normal file
@ -0,0 +1,144 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# *****************************************************************************
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Module authors:
|
||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||
#
|
||||
# *****************************************************************************
|
||||
|
||||
"""driver for cryotel stirling cryocooler"""
|
||||
|
||||
from frappy.core import Command, EnumType, FloatRange, HasIO, Parameter, Drivable, StringIO, StringType
|
||||
from frappy.errors import CommunicationFailedError, HardwareError
|
||||
|
||||
|
||||
class CryotelIO(StringIO):
|
||||
end_of_line = ('\n', '\r')
|
||||
# eol_write will be given explicitly
|
||||
|
||||
@Command(StringType(), result=StringType())
|
||||
def communicate(self, command):
|
||||
"""send a command and receive a reply
|
||||
|
||||
we receive an echo line first.
|
||||
in case the command does not contain '=', the effective reply is in a second line
|
||||
"""
|
||||
with self._lock:
|
||||
echo = super().communicate(command)
|
||||
if echo.strip() != command.strip():
|
||||
raise CommunicationFailedError('missing echo')
|
||||
if '=' in command:
|
||||
reply = echo
|
||||
else:
|
||||
reply = self._conn.readline().decode()
|
||||
return reply
|
||||
|
||||
|
||||
class Cryo(HasIO, Drivable):
|
||||
value = Parameter('current temperature', FloatRange(unit='deg'))
|
||||
target = Parameter('target temperature', FloatRange(unit='deg'), readonly=False)
|
||||
mode = Parameter('control mode', EnumType('mode', off=0, power=1, temperature=2), readonly=False)
|
||||
power = Parameter('power', FloatRange(unit='W'))
|
||||
setpower = Parameter('requested power', FloatRange(unit='W'), default=0)
|
||||
cool_power = Parameter('power for cooling', FloatRange(unit='W'), default=240, readonly=False)
|
||||
hold_power = Parameter('power for holding T', FloatRange(unit='W'), default=120, readonly=False)
|
||||
cool_threshold = Parameter('switch to cool_power once above this value', FloatRange(unit='K'), default=100, readonly=False)
|
||||
hold_threshold = Parameter('switch to hold_power once below this value', FloatRange(unit='K'), default=95, readonly=False)
|
||||
|
||||
ioClass = CryotelIO
|
||||
cnt_inside = 0
|
||||
|
||||
def get(self, cmd):
|
||||
return float(self.communicate(cmd))
|
||||
|
||||
def set(self, cmd, value, check=False):
|
||||
setcmd = '%s=%.2f' % (cmd, value)
|
||||
self.communicate(setcmd)
|
||||
reply = float(self.communicate(cmd))
|
||||
if check:
|
||||
if value != reply:
|
||||
raise HardwareError('illegal reply from %s: %g' % (cmd, reply))
|
||||
return reply
|
||||
|
||||
def read_value(self):
|
||||
temp = self.get('TC')
|
||||
status = self.status
|
||||
if self.mode == 1: # P reg
|
||||
setpower = self.setpower
|
||||
if temp < self.hold_threshold:
|
||||
setpower = self.hold_power
|
||||
status = self.Status.IDLE, 'holding'
|
||||
elif temp > self.cool_threshold:
|
||||
setpower = self.cool_power
|
||||
status = self.Status.BUSY, 'cooling'
|
||||
if abs(setpower - self.setpower) > 0.009:
|
||||
self.setpower = self.set('SET PWOUT', setpower)
|
||||
elif self.mode == 2: # T reg
|
||||
if self.status[0] == 'BUSY':
|
||||
if abs(temp - self.target) < 1:
|
||||
self.cnt_inside += 1
|
||||
if self.cnt_inside >= 10:
|
||||
status = self.Status.IDLE, ''
|
||||
else:
|
||||
status = self.Status.BUSY, 'settling'
|
||||
else:
|
||||
status = self.Status.BUSY, 'outside tolerance'
|
||||
else:
|
||||
status = self.Status.IDLE, 'off'
|
||||
if status != self.status:
|
||||
self.status = status
|
||||
return temp
|
||||
|
||||
def read_target(self):
|
||||
return self.get('SET TTARGET')
|
||||
|
||||
def write_target(self, value):
|
||||
if abs(value - self.target) > 0.009:
|
||||
self.status = self.Status.BUSY, 'changed target'
|
||||
self.cnt_inside = 0
|
||||
return self.set('SET TTARGET', value)
|
||||
|
||||
def read_power(self):
|
||||
return self.get('P')
|
||||
|
||||
def read_setpower(self):
|
||||
return self.get('SET PWOUT')
|
||||
|
||||
def read_mode(self):
|
||||
is_stopped = self.get('SET SSTOP')
|
||||
if is_stopped:
|
||||
return 0 # off
|
||||
pidmode = self.get('SET PID')
|
||||
if pidmode == 2:
|
||||
return 2 # T reg
|
||||
return 1 # P reg
|
||||
|
||||
def write_mode(self, value):
|
||||
if value == 0:
|
||||
self.set('SET SSTOP', 1, check=True)
|
||||
self.status = self.Status.IDLE, 'off'
|
||||
return value
|
||||
is_stopped = self.get('SET SSTOP')
|
||||
if is_stopped:
|
||||
self.set('SET SSTOP', 0, check=True)
|
||||
if value == 1:
|
||||
self.set('SET PID', 0, check=True)
|
||||
self.read_value()
|
||||
else:
|
||||
self.set('SET PID', 2, check=True)
|
||||
self.write_target(self.target)
|
||||
return value
|
62
frappy_psi/dg645.py
Normal file
62
frappy_psi/dg645.py
Normal file
@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# *****************************************************************************
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Module authors:
|
||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||
# *****************************************************************************
|
||||
"""Delay generator stanford 645"""
|
||||
|
||||
from frappy.core import FloatRange, HasIO, Module, Parameter, StringIO
|
||||
|
||||
|
||||
class DG645(StringIO):
|
||||
end_of_line = '\n'
|
||||
|
||||
|
||||
class Delay(HasIO, Module):
|
||||
|
||||
on1 = Parameter('on delay 1', FloatRange(unit='sec'), readonly=False, default=0)
|
||||
off1 = Parameter('off delay 1', FloatRange(unit='sec'), readonly=False, default=60e-9)
|
||||
on2 = Parameter('on delay 2', FloatRange(unit='sec'), readonly=False, default=0)
|
||||
off2 = Parameter('off delay 2', FloatRange(unit='sec'), readonly=False, default=150e-9)
|
||||
|
||||
ioClass = DG645
|
||||
|
||||
def read_on1(self):
|
||||
return float(self.communicate('DLAY?2').split(',')[1])
|
||||
|
||||
def read_off1(self):
|
||||
return float(self.communicate('DLAY?3').split(',')[1])
|
||||
|
||||
def read_on2(self):
|
||||
return float(self.communicate('DLAY?4').split(',')[1])
|
||||
|
||||
def read_off2(self):
|
||||
return float(self.communicate('DLAY?5').split(',')[1])
|
||||
|
||||
def write_on1(self, value):
|
||||
return float(self.communicate('DLAY 2,0,%g;DLAY?2' % value).split(',')[1])
|
||||
|
||||
def write_off1(self, value):
|
||||
result = self.communicate('DLAY 3,0,%g;DLAY?3' % value)
|
||||
return float(result.split(',')[1])
|
||||
|
||||
def write_on2(self, value):
|
||||
return float(self.communicate('DLAY 4,0,%g;DLAY?4' % value).split(',')[1])
|
||||
|
||||
def write_off2(self, value):
|
||||
return float(self.communicate('DLAY 5,0,%g;DLAY?5' % value).split(',')[1])
|
97
frappy_psi/dilsc.py
Normal file
97
frappy_psi/dilsc.py
Normal file
@ -0,0 +1,97 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# *****************************************************************************
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Module authors:
|
||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||
# *****************************************************************************
|
||||
"""vector field"""
|
||||
|
||||
from frappy.core import Drivable, Done, BUSY, IDLE, WARN, ERROR
|
||||
from frappy.errors import BadValueError
|
||||
from frappy_psi.vector import Vector
|
||||
|
||||
|
||||
DECREASE = 1
|
||||
INCREASE = 2
|
||||
|
||||
|
||||
class VectorField(Vector, Drivable):
|
||||
_state = None
|
||||
|
||||
def doPoll(self):
|
||||
"""periodically called method"""
|
||||
try:
|
||||
if self._starting:
|
||||
# first decrease components
|
||||
driving = False
|
||||
for target, component in zip(self.target, self.components):
|
||||
if target * component.value < 0:
|
||||
# change sign: drive to zero first
|
||||
target = 0
|
||||
if abs(target) < abs(component.target):
|
||||
if target != component.target:
|
||||
component.write_target(target)
|
||||
if component.isDriving():
|
||||
driving = True
|
||||
if driving:
|
||||
return
|
||||
# now we can go to the final targets
|
||||
for target, component in zip(self.target, self.components):
|
||||
component.write_target(target)
|
||||
self._starting = False
|
||||
else:
|
||||
for component in self.components:
|
||||
if component.isDriving():
|
||||
return
|
||||
self.setFastPoll(False)
|
||||
finally:
|
||||
super().doPoll()
|
||||
|
||||
def merge_status(self):
|
||||
names = [c.name for c in self.components if c.status[0] >= ERROR]
|
||||
if names:
|
||||
return ERROR, 'error in %s' % ', '.join(names)
|
||||
names = [c.name for c in self.components if c.isDriving()]
|
||||
if self._state:
|
||||
# self.log.info('merge %r', [c.status for c in self.components])
|
||||
if names:
|
||||
direction = 'down ' if self._state == DECREASE else ''
|
||||
return BUSY, 'ramping %s%s' % (direction, ', '.join(names))
|
||||
if self.status[0] == BUSY:
|
||||
return self.status
|
||||
return BUSY, 'driving'
|
||||
if names:
|
||||
return WARN, 'moving %s directly' % ', '.join(names)
|
||||
names = [c.name for c in self.components if c.status[0] >= WARN]
|
||||
if names:
|
||||
return WARN, 'warnings in %s' % ', '.join(names)
|
||||
return IDLE, ''
|
||||
|
||||
def write_target(self, value):
|
||||
"""initiate target change"""
|
||||
# first make sure target is valid
|
||||
for target, component in zip(self.target, self.components):
|
||||
# check against limits if individual components
|
||||
component.check_limits(target)
|
||||
if sum(v * v for v in value) > 1:
|
||||
raise BadValueError('norm of vector too high')
|
||||
self.log.info('decrease')
|
||||
self.setFastPoll(True)
|
||||
self.target = value
|
||||
self._state = DECREASE
|
||||
self.doPoll()
|
||||
self.log.info('done write_target %r', value)
|
||||
return Done
|
125
frappy_psi/dpm.py
Normal file
125
frappy_psi/dpm.py
Normal file
@ -0,0 +1,125 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# *****************************************************************************
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Module authors:
|
||||
# M. Zolliker <markus.zolliker@psi.ch>
|
||||
#
|
||||
# *****************************************************************************
|
||||
"""transducer DPM3 read out"""
|
||||
|
||||
from frappy.core import Readable, Parameter, FloatRange, StringIO,\
|
||||
HasIO, IntRange, Done
|
||||
|
||||
|
||||
class DPM3IO(StringIO):
|
||||
end_of_line = '\r'
|
||||
timeout = 3
|
||||
identification = [('*1R135', '01')]
|
||||
|
||||
|
||||
def hex2float(hexvalue, digits):
|
||||
value = int(hexvalue, 16)
|
||||
if value >= 0x800000:
|
||||
value -= 0x1000000
|
||||
return value / (10 ** digits)
|
||||
|
||||
|
||||
def float2hex(value, digits):
|
||||
intvalue = int(round(value * 10 ** digits,0))
|
||||
if intvalue < 0:
|
||||
intvalue += 0x1000000
|
||||
return '%06X' % intvalue
|
||||
|
||||
|
||||
class DPM3(HasIO, Readable):
|
||||
OFFSET = 0x8f
|
||||
SCALE = 0x8c
|
||||
|
||||
MAGNITUDE = {'1': 1, '2': 10, '3': 100, '4': 1e3, '5': 1e4, '6': 1e5,
|
||||
'9': -1, 'A': -10, 'B': -100, 'C': -1e3, 'D': -1e4, 'E': -1e5}
|
||||
|
||||
ioClass = DPM3IO
|
||||
|
||||
value = Parameter(datatype=FloatRange(unit='N'))
|
||||
digits = Parameter('number of digits for value', IntRange(0, 5), initwrite=True, readonly=False)
|
||||
# Note: we have to treat the units properly.
|
||||
# We got an output of 150 for 10N. The maximal force we want to deal with is 100N,
|
||||
# thus a maximal output of 1500. 10=150/f
|
||||
offset = Parameter('', FloatRange(-1e5, 1e5), readonly=False)
|
||||
scale_factor = Parameter('', FloatRange(-1e5, 1e5, unit='input_units/N'), readonly=False)
|
||||
|
||||
def query(self, adr, value=None):
|
||||
if value is not None:
|
||||
if adr == self.SCALE:
|
||||
absval = abs(value)
|
||||
for nibble, mag in self.MAGNITUDE.items():
|
||||
if 10000 <= round(value * mag, 0) <= 99999:
|
||||
break
|
||||
else:
|
||||
# no suitable range found
|
||||
if absval >= 99999.5: # overrange
|
||||
raise ValueError('%s is out of range' % value)
|
||||
# underrange: take lowest
|
||||
nibble = '9' if value < 0 else '1'
|
||||
mag = self.MAGNITUDE[nibble]
|
||||
hex_val = nibble + '%05X' % int(round(value * mag, 0))
|
||||
if hex_val[1:] == '00000':
|
||||
raise ValueError('scale factor can not be 0', value)
|
||||
else:
|
||||
hex_val = float2hex(value, self.digits)
|
||||
cmd = '*1F3%02X%s\r' % (adr, hex_val)
|
||||
else:
|
||||
cmd = ""
|
||||
cmd = cmd + '*1G3%02X' % adr
|
||||
hexvalue = self.communicate(cmd)
|
||||
if adr == self.SCALE:
|
||||
mag = self.MAGNITUDE[hexvalue[0:1]]
|
||||
value = int(hexvalue[1:], 16)
|
||||
return value/mag
|
||||
else:
|
||||
return hex2float(hexvalue, self.digits)
|
||||
|
||||
def write_digits(self, value):
|
||||
# value defines the number of digits
|
||||
back_value = self.communicate('*1F135%02X\r*1G135' % (value + 1))
|
||||
self.digits = int(back_value, 16) - 1
|
||||
# recalculate proper scale and offset
|
||||
self.write_scale_factor(self.scale_factor)
|
||||
self.write_offset(self.offset)
|
||||
return Done
|
||||
|
||||
def read_digits(self):
|
||||
back_value = self.communicate('*1G135')
|
||||
return int(back_value,16) - 1
|
||||
|
||||
def read_value(self):
|
||||
return float(self.communicate('*1B1'))
|
||||
|
||||
def read_offset(self):
|
||||
reply = self.query(self.OFFSET)
|
||||
return reply
|
||||
|
||||
def write_offset(self, value):
|
||||
return self.query(self.OFFSET, value)
|
||||
|
||||
def read_scale_factor(self):
|
||||
reply = self.query(self.SCALE)
|
||||
return float(reply) / 10 ** self.digits
|
||||
|
||||
def write_scale_factor(self, value):
|
||||
reply = self.query(self.SCALE, value * 10 ** self.digits)
|
||||
return float(reply) / 10 ** self.digits
|
329
frappy_psi/ips_mercury.py
Normal file
329
frappy_psi/ips_mercury.py
Normal file
@ -0,0 +1,329 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# *****************************************************************************
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Module authors:
|
||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||
# *****************************************************************************
|
||||
"""oxford instruments mercury IPS power supply"""
|
||||
|
||||
import time
|
||||
from frappy.core import Parameter, EnumType, FloatRange, BoolType, IntRange, StringType, Property, BUSY
|
||||
from frappy.lib.enum import Enum
|
||||
from frappy.errors import BadValueError, HardwareError
|
||||
from frappy_psi.magfield import Magfield, SimpleMagfield, Status
|
||||
from frappy_psi.mercury import MercuryChannel, off_on, Mapped
|
||||
from frappy.states import Retry
|
||||
|
||||
Action = Enum(hold=0, run_to_set=1, run_to_zero=2, clamped=3)
|
||||
hold_rtoz_rtos_clmp = Mapped(HOLD=Action.hold, RTOS=Action.run_to_set,
|
||||
RTOZ=Action.run_to_zero, CLMP=Action.clamped)
|
||||
CURRENT_CHECK_SIZE = 2
|
||||
|
||||
|
||||
class SimpleField(MercuryChannel, SimpleMagfield):
|
||||
nunits = Property('number of IPS subunits', IntRange(1, 6), default=1)
|
||||
action = Parameter('action', EnumType(Action), readonly=False)
|
||||
setpoint = Parameter('field setpoint', FloatRange(unit='T'), default=0)
|
||||
voltage = Parameter('leads voltage', FloatRange(unit='V'), default=0)
|
||||
atob = Parameter('field to amp', FloatRange(0, unit='A/T'), default=0)
|
||||
working_ramp = Parameter('effective ramp', FloatRange(0, unit='T/min'), default=0)
|
||||
channel_type = 'PSU'
|
||||
slave_currents = None
|
||||
classdict = {}
|
||||
|
||||
def __new__(cls, name, logger, cfgdict, srv):
|
||||
base = cls.__bases__[1]
|
||||
nunits = cfgdict.get('nunits', 1)
|
||||
if nunits == 1:
|
||||
obj = object.__new__(cls)
|
||||
return obj
|
||||
classname = cls.__name__ + str(nunits)
|
||||
newclass = cls.classdict.get(classname)
|
||||
if not newclass:
|
||||
# create individual current and voltage parameters dynamically
|
||||
attrs = {}
|
||||
for i in range(1, nunits + 1):
|
||||
attrs['I%d' % i] = Parameter('slave %s current' % i, FloatRange(unit='A'), default=0)
|
||||
attrs['V%d' % i] = Parameter('slave %s voltage' % i, FloatRange(unit='V'), default=0)
|
||||
|
||||
newclass = type(classname, (cls,), attrs)
|
||||
cls.classdict[classname] = newclass
|
||||
obj = object.__new__(newclass)
|
||||
return obj
|
||||
|
||||
def initModule(self):
|
||||
super().initModule()
|
||||
try:
|
||||
self.write_action(Action.hold)
|
||||
except Exception as e:
|
||||
self.log.error('can not set to hold %r', e)
|
||||
|
||||
def read_value(self):
|
||||
return self.query('PSU:SIG:FLD')
|
||||
|
||||
def read_ramp(self):
|
||||
return self.query('PSU:SIG:RFST')
|
||||
|
||||
def write_ramp(self, value):
|
||||
return self.change('PSU:SIG:RFST', value)
|
||||
|
||||
def read_action(self):
|
||||
return self.query('PSU:ACTN', hold_rtoz_rtos_clmp)
|
||||
|
||||
def write_action(self, value):
|
||||
return self.change('PSU:ACTN', value, hold_rtoz_rtos_clmp)
|
||||
|
||||
def read_atob(self):
|
||||
return self.query('PSU:ATOB')
|
||||
|
||||
def read_voltage(self):
|
||||
return self.query('PSU:SIG:VOLT')
|
||||
|
||||
def read_working_ramp(self):
|
||||
return self.query('PSU:SIG:RFLD')
|
||||
|
||||
def read_setpoint(self):
|
||||
return self.query('PSU:SIG:FSET')
|
||||
|
||||
def set_and_go(self, value):
|
||||
self.setpoint = self.change('PSU:SIG:FSET', value)
|
||||
assert self.write_action(Action.hold) == Action.hold
|
||||
assert self.write_action(Action.run_to_set) == Action.run_to_set
|
||||
|
||||
def start_ramp_to_target(self, sm):
|
||||
# if self.action != Action.hold:
|
||||
# assert self.write_action(Action.hold) == Action.hold
|
||||
# return Retry
|
||||
self.set_and_go(sm.target)
|
||||
sm.try_cnt = 5
|
||||
return self.ramp_to_target
|
||||
|
||||
def ramp_to_target(self, sm):
|
||||
try:
|
||||
return super().ramp_to_target(sm)
|
||||
except HardwareError:
|
||||
sm.try_cnt -= 1
|
||||
if sm.try_cnt < 0:
|
||||
raise
|
||||
self.set_and_go(sm.target)
|
||||
return Retry
|
||||
|
||||
def final_status(self, *args, **kwds):
|
||||
self.write_action(Action.hold)
|
||||
return super().final_status(*args, **kwds)
|
||||
|
||||
def on_restart(self, sm):
|
||||
self.write_action(Action.hold)
|
||||
return super().on_restart(sm)
|
||||
|
||||
|
||||
class Field(SimpleField, Magfield):
|
||||
persistent_field = Parameter(
|
||||
'persistent field', FloatRange(unit='$'), readonly=False)
|
||||
wait_switch_on = Parameter(
|
||||
'wait time to ensure switch is on', FloatRange(0, unit='s'), readonly=True, default=60)
|
||||
wait_switch_off = Parameter(
|
||||
'wait time to ensure switch is off', FloatRange(0, unit='s'), readonly=True, default=60)
|
||||
forced_persistent_field = Parameter(
|
||||
'manual indication that persistent field is bad', BoolType(), readonly=False, default=False)
|
||||
|
||||
_field_mismatch = None
|
||||
__init = True
|
||||
__switch_fixed_until = 0
|
||||
|
||||
def doPoll(self):
|
||||
super().doPoll()
|
||||
self.read_current()
|
||||
|
||||
def startModule(self, start_events):
|
||||
# on restart, assume switch is changed long time ago, if not, the mercury
|
||||
# will complain and this will be handled in start_ramp_to_field
|
||||
self.switch_on_time = 0
|
||||
self.switch_off_time = 0
|
||||
self.switch_heater = self.query('PSU:SIG:SWHT', off_on)
|
||||
super().startModule(start_events)
|
||||
|
||||
def read_value(self):
|
||||
current = self.query('PSU:SIG:FLD')
|
||||
pf = self.query('PSU:SIG:PFLD')
|
||||
if self.__init:
|
||||
self.__init = False
|
||||
self.persistent_field = pf
|
||||
if self.switch_heater == self.switch_heater.on or self._field_mismatch is None:
|
||||
self.forced_persistent_field = False
|
||||
self._field_mismatch = False
|
||||
return current
|
||||
self._field_mismatch = abs(self.persistent_field - pf) > self.tolerance
|
||||
return pf
|
||||
|
||||
def read_current(self):
|
||||
if self.slave_currents is None:
|
||||
self.slave_currents = [[] for _ in range(self.nunits + 1)]
|
||||
if self.nunits > 1:
|
||||
for i in range(1, self.nunits + 1):
|
||||
curri = self.query('DEV:PSU.M%d:PSU:SIG:CURR' % i)
|
||||
volti = self.query('DEV:PSU.M%d:PSU:SIG:VOLT' % i)
|
||||
setattr(self, 'I%d' % i, curri)
|
||||
setattr(self, 'V%d' % i, volti)
|
||||
self.slave_currents[i].append(curri)
|
||||
current = self.query('PSU:SIG:CURR')
|
||||
self.slave_currents[0].append(current)
|
||||
min_ = min(self.slave_currents[0]) / self.nunits
|
||||
max_ = max(self.slave_currents[0]) / self.nunits
|
||||
# keep one element more for the total current (first and last measurement is a total)
|
||||
self.slave_currents[0] = self.slave_currents[0][-CURRENT_CHECK_SIZE-1:]
|
||||
for i in range(1, self.nunits + 1):
|
||||
min_i = min(self.slave_currents[i])
|
||||
max_i = max(self.slave_currents[i])
|
||||
if len(self.slave_currents[i]) > CURRENT_CHECK_SIZE:
|
||||
self.slave_currents[i] = self.slave_currents[i][-CURRENT_CHECK_SIZE:]
|
||||
if min_i - 0.1 > max_ or min_ > max_i + 0.1: # use an arbitrary 0.1 A tolerance
|
||||
self.log.warning('individual currents mismatch %r', self.slave_currents)
|
||||
else:
|
||||
current = self.query('PSU:SIG:CURR')
|
||||
if self.atob:
|
||||
return current / self.atob
|
||||
return 0
|
||||
|
||||
def write_persistent_field(self, value):
|
||||
if self.forced_persistent_field:
|
||||
self._field_mismatch = False
|
||||
return value
|
||||
raise BadValueError('changing persistent field needs forced_persistent_field=True')
|
||||
|
||||
def write_target(self, target):
|
||||
if self._field_mismatch:
|
||||
self.forced_persistent_field = True
|
||||
raise BadValueError('persistent field does not match - set persistent field to guessed value first')
|
||||
return super().write_target(target)
|
||||
|
||||
def read_switch_heater(self):
|
||||
value = self.query('PSU:SIG:SWHT', off_on)
|
||||
now = time.time()
|
||||
if value != self.switch_heater:
|
||||
if now < self.__switch_fixed_until:
|
||||
self.log.debug('correct fixed switch time')
|
||||
# probably switch heater was changed, but IPS reply is not yet updated
|
||||
if self.switch_heater:
|
||||
self.switch_on_time = time.time()
|
||||
else:
|
||||
self.switch_off_time = time.time()
|
||||
return self.switch_heater
|
||||
return value
|
||||
|
||||
def read_wait_switch_on(self):
|
||||
return self.query('PSU:SWONT') * 0.001
|
||||
|
||||
def read_wait_switch_off(self):
|
||||
return self.query('PSU:SWOFT') * 0.001
|
||||
|
||||
def write_switch_heater(self, value):
|
||||
if value == self.read_switch_heater():
|
||||
self.log.info('switch heater already %r', value)
|
||||
# we do not want to restart the timer
|
||||
return value
|
||||
self.__switch_fixed_until = time.time() + 10
|
||||
self.log.debug('switch time fixed for 10 sec')
|
||||
result = self.change('PSU:SIG:SWHT', value, off_on, n_retry=0) # no readback check
|
||||
return result
|
||||
|
||||
def start_ramp_to_field(self, sm):
|
||||
if abs(self.current - self.persistent_field) <= self.tolerance:
|
||||
self.log.info('leads %g are already at %g', self.current, self.persistent_field)
|
||||
return self.ramp_to_field
|
||||
try:
|
||||
self.set_and_go(self.persistent_field)
|
||||
except (HardwareError, AssertionError) as e:
|
||||
if self.switch_heater:
|
||||
self.log.warn('switch is already on!')
|
||||
return self.ramp_to_field
|
||||
self.log.warn('wait first for switch off current=%g pf=%g %r', self.current, self.persistent_field, e)
|
||||
sm.after_wait = self.ramp_to_field
|
||||
return self.wait_for_switch
|
||||
return self.ramp_to_field
|
||||
|
||||
def start_ramp_to_target(self, sm):
|
||||
sm.try_cnt = 5
|
||||
try:
|
||||
self.set_and_go(sm.target)
|
||||
except (HardwareError, AssertionError) as e:
|
||||
self.log.warn('switch not yet ready %r', e)
|
||||
self.status = Status.PREPARING, 'wait for switch on'
|
||||
sm.after_wait = self.ramp_to_target
|
||||
return self.wait_for_switch
|
||||
return self.ramp_to_target
|
||||
|
||||
def ramp_to_field(self, sm):
|
||||
try:
|
||||
return super().ramp_to_field(sm)
|
||||
except HardwareError:
|
||||
sm.try_cnt -= 1
|
||||
if sm.try_cnt < 0:
|
||||
raise
|
||||
self.set_and_go(sm.persistent_field)
|
||||
return Retry
|
||||
|
||||
def wait_for_switch(self, sm):
|
||||
if not sm.delta(10):
|
||||
return Retry
|
||||
try:
|
||||
self.log.warn('try again')
|
||||
# try again
|
||||
self.set_and_go(self.persistent_field)
|
||||
except (HardwareError, AssertionError) as e:
|
||||
return Retry
|
||||
return sm.after_wait
|
||||
|
||||
def wait_for_switch_on(self, sm):
|
||||
self.read_switch_heater() # trigger switch_on/off_time
|
||||
if self.switch_heater == self.switch_heater.OFF:
|
||||
if sm.init: # avoid too many states chained
|
||||
return Retry
|
||||
self.log.warning('switch turned off manually?')
|
||||
return self.start_switch_on
|
||||
return super().wait_for_switch_on(sm)
|
||||
|
||||
def wait_for_switch_off(self, sm):
|
||||
self.read_switch_heater()
|
||||
if self.switch_heater == self.switch_heater.ON:
|
||||
if sm.init: # avoid too many states chained
|
||||
return Retry
|
||||
self.log.warning('switch turned on manually?')
|
||||
return self.start_switch_off
|
||||
return super().wait_for_switch_off(sm)
|
||||
|
||||
def start_ramp_to_zero(self, sm):
|
||||
try:
|
||||
assert self.write_action(Action.hold) == Action.hold
|
||||
assert self.write_action(Action.run_to_zero) == Action.run_to_zero
|
||||
except (HardwareError, AssertionError) as e:
|
||||
self.log.warn('switch not yet ready %r', e)
|
||||
self.status = Status.PREPARING, 'wait for switch off'
|
||||
sm.after_wait = self.ramp_to_zero
|
||||
return self.wait_for_switch
|
||||
return self.ramp_to_zero
|
||||
|
||||
def ramp_to_zero(self, sm):
|
||||
try:
|
||||
return super().ramp_to_zero(sm)
|
||||
except HardwareError:
|
||||
sm.try_cnt -= 1
|
||||
if sm.try_cnt < 0:
|
||||
raise
|
||||
assert self.write_action(Action.hold) == Action.hold
|
||||
assert self.write_action(Action.run_to_zero) == Action.run_to_zero
|
||||
return Retry
|
102
frappy_psi/iqplot.py
Normal file
102
frappy_psi/iqplot.py
Normal file
@ -0,0 +1,102 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Created on Tue Feb 4 11:07:56 2020
|
||||
|
||||
@author: tartarotti_d-adm
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
def rect(x1, x2, y1, y2):
|
||||
return np.array([[x1,x2,x2,x1,x1],[y1,y1,y2,y2,y1]])
|
||||
|
||||
NAN = float('nan')
|
||||
|
||||
def rects(intervals, y12):
|
||||
result = [rect(*intervals[0], *y12)]
|
||||
for x12 in intervals[1:]:
|
||||
result.append([[NAN],[NAN]])
|
||||
result.append(rect(*x12, *y12))
|
||||
return np.concatenate(result, axis=1)
|
||||
|
||||
class Plot:
|
||||
def __init__(self, maxy):
|
||||
self.lines = {}
|
||||
self.yaxis = ((-2 * maxy, maxy), (-maxy, 2 * maxy))
|
||||
self.first = True
|
||||
self.fig = None
|
||||
|
||||
def set_line(self, iax, name, data, fmt, **kwds):
|
||||
"""
|
||||
plot or update a line
|
||||
|
||||
when called with self.first = True: plot the line
|
||||
when called with self.first = False: update the line
|
||||
|
||||
iax: 0: left, 1: right yaxis
|
||||
name: the name of the line. used also as label for legend, if not starting with underscore
|
||||
data: data[0] and data[1] are used for x/y data respectively
|
||||
fmt, other keywords: forwarded to <axis>.plot
|
||||
"""
|
||||
# ax: 0: left, 1: right
|
||||
if self.first:
|
||||
if name.startswith('_'):
|
||||
label = '_nolegend_'
|
||||
else:
|
||||
label = name
|
||||
self.lines[name], = self.ax[iax].plot(data[0], data[1], fmt, label=label, **kwds)
|
||||
else:
|
||||
self.lines[name].set_data(data[0:2])
|
||||
|
||||
def close(self):
|
||||
if self.fig:
|
||||
plt.close(self.fig)
|
||||
self.fig = None
|
||||
self.first = True
|
||||
|
||||
def plot(self, curves, rois=None, average=None):
|
||||
boxes = rects(rois[1:], self.yaxis[0])
|
||||
pbox = rect(*rois[0], *self.yaxis[1])
|
||||
rbox = rect(*rois[1], *self.yaxis[0])
|
||||
|
||||
pshift = self.yaxis[0][1] * 0.5
|
||||
pulse = curves[3] - pshift
|
||||
# normalized to 0.8 * pshift:
|
||||
#pulse = (curves[3] / np.max(curves[3]))* pshift * 0.8 - pshift
|
||||
|
||||
try:
|
||||
if self.first:
|
||||
plt.ion()
|
||||
self.fig, axleft = plt.subplots(figsize=(15,7))
|
||||
plt.title("I/Q", fontsize=14)
|
||||
axleft.set_xlim(0, curves[0][-1])
|
||||
self.ax = [axleft, axleft.twinx()]
|
||||
self.ax[0].axhline(y=0, color='#cccccc') # show x-axis line
|
||||
self.ax[1].axhline(y=0, color='#cccccc')
|
||||
self.ax[0].set_ylim(*self.yaxis[0])
|
||||
self.ax[1].set_ylim(*self.yaxis[1])
|
||||
|
||||
self.set_line(0, "I", curves, 'b-') # using curves [0] and [1]
|
||||
self.set_line(0, "_Iaverage", average, 'b.')
|
||||
|
||||
self.set_line(0, "Ampl", (curves[0],np.sqrt(curves[1]**2+curves[2]**2)), '#808080')
|
||||
|
||||
self.set_line(1, "Q", (curves[0], curves[2]), 'g-')
|
||||
self.set_line(1, "_Qaverage", (average[0], average[2]), 'g.')
|
||||
|
||||
self.set_line(0, "pulse", (curves[0], pulse), 'c-')
|
||||
|
||||
self.set_line(0, "roi's", boxes, 'm-')
|
||||
self.set_line(1, "pulse reg", pbox, 'k-')
|
||||
self.set_line(0, "ctrl reg", rbox, 'r-')
|
||||
|
||||
if self.first:
|
||||
self.fig.legend(fontsize=12)
|
||||
plt.tight_layout()
|
||||
finally:
|
||||
self.first = False
|
||||
|
||||
plt.draw()
|
||||
self.fig.canvas.draw()
|
||||
self.fig.canvas.flush_events()
|
105
frappy_psi/ls240.py
Normal file
105
frappy_psi/ls240.py
Normal file
@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# *****************************************************************************
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Module authors:
|
||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||
# *****************************************************************************
|
||||
"""LakeShore 240 temperature monitor
|
||||
|
||||
experimental, does not really work
|
||||
"""
|
||||
|
||||
import struct
|
||||
from frappy.core import FloatRange, HasIodev, Readable, Parameter, BytesIO
|
||||
from frappy.errors import CommunicationFailedError
|
||||
|
||||
SD1= 0x10
|
||||
SD2= 0x68
|
||||
FC = 0x49
|
||||
ED = 0x16
|
||||
|
||||
STATUS_REQ = 0x49
|
||||
|
||||
def dehex(msg):
|
||||
return bytes(int(b, 16) for b in msg.split())
|
||||
|
||||
def hexify(msg):
|
||||
return ' '.join('%2.2X' % b for b in msg)
|
||||
|
||||
|
||||
class Ls240(HasIodev, Readable):
|
||||
value = Parameter('sensor reading', FloatRange(unit='Ohm'))
|
||||
|
||||
iodevClass = BytesIO
|
||||
|
||||
def request(self, replylen, data='', ext=None, dst_adr=3, src_adr=2):
|
||||
data = dehex(data)
|
||||
if ext is None:
|
||||
ext = len(data) > 1
|
||||
if ext:
|
||||
dst_adr |= 0x80
|
||||
src_adr |= 0x80
|
||||
if len(data) > 1:
|
||||
length = len(data) + 2
|
||||
hdr = [SD2, length, length, SD2]
|
||||
else:
|
||||
hdr = [SD1]
|
||||
mid = [dst_adr, src_adr] + list(data)
|
||||
checksum = sum(mid) % 256
|
||||
msg = bytes(hdr + mid + [checksum, ED])
|
||||
for i in range(10):
|
||||
try:
|
||||
# print('>', hexify(msg))
|
||||
reply = self._iodev.communicate(msg, replylen)
|
||||
# print('<', hexify(reply))
|
||||
except (TimeoutError, CommunicationFailedError):
|
||||
continue
|
||||
return reply
|
||||
return None
|
||||
|
||||
def read_value(self):
|
||||
|
||||
# check connection
|
||||
self.request(6, '49')
|
||||
|
||||
# get diag
|
||||
# 3C: slave diag, (what means 3E?)
|
||||
reply = self.request(17, '6D 3C 3E')
|
||||
assert reply[13:15] == b'\x0f\x84' # LS240 ident
|
||||
|
||||
# set parameters
|
||||
# 3D set param (what means 3E?)
|
||||
# B0 FF FF: no watchdog, 00: min wait, 0F 84: ident, 01: group
|
||||
assert b'\xe5' == self.request(1, '5D 3D 3E B0 FF FF 00 0F 84 01')
|
||||
|
||||
# set config
|
||||
# 3E set config (what means 2nd 3E?)
|
||||
# 93: input only, 4 == 3+1 bytes
|
||||
assert b'\xe5' == self.request(1, '7D 3E 3E 93')
|
||||
|
||||
# get diag
|
||||
# 3C: slave diag, (what means 3E?)
|
||||
reply = self.request(17, '5D 3C 3E')
|
||||
assert reply[13:15] == b'\x0f\x84' # LS240 ident
|
||||
|
||||
# get data
|
||||
# do not know what 42 24 means
|
||||
reply = self.request(13, '7D 42 24', ext=0)
|
||||
print('DATA', reply)
|
||||
value = struct.unpack('>f', reply[7:11])[0]
|
||||
print('VALUE', value)
|
||||
return value
|
45
frappy_psi/ls340res.py
Normal file
45
frappy_psi/ls340res.py
Normal file
@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# *****************************************************************************
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Module authors:
|
||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||
# *****************************************************************************
|
||||
"""very simple LakeShore Model 340 driver, resistivity only"""
|
||||
|
||||
import time
|
||||
|
||||
from frappy.datatypes import StringType, FloatRange
|
||||
from frappy.modules import Parameter, Property, Readable
|
||||
from frappy.io import HasIO, StringIO
|
||||
|
||||
|
||||
class LscIO(StringIO):
|
||||
identification = [('*IDN?', 'LSCI,MODEL340,.*')]
|
||||
end_of_line = '\r'
|
||||
wait_before = 0.05
|
||||
|
||||
|
||||
class ResChannel(HasIO, Readable):
|
||||
"""temperature channel on Lakeshore 340"""
|
||||
|
||||
ioClass = LscIO
|
||||
|
||||
value = Parameter(datatype=FloatRange(unit='Ohm'))
|
||||
channel = Property('the channel A,B,C or D', StringType())
|
||||
|
||||
def read_value(self):
|
||||
return float(self.communicate('SRDG?%s' % self.channel))
|
75
frappy_psi/ls370sim.py
Normal file
75
frappy_psi/ls370sim.py
Normal file
@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# *****************************************************************************
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Module authors:
|
||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||
# *****************************************************************************
|
||||
"""a very simple simulator for a LakeShore Model 370"""
|
||||
|
||||
from frappy.modules import Communicator
|
||||
|
||||
|
||||
class Ls370Sim(Communicator):
|
||||
CHANNEL_COMMANDS = [
|
||||
('RDGR?%d', '1.0'),
|
||||
('RDGST?%d', '0'),
|
||||
('RDGRNG?%d', '0,5,5,0,0'),
|
||||
('INSET?%d', '1,5,5,0,0'),
|
||||
('FILTER?%d', '1,5,80'),
|
||||
]
|
||||
OTHER_COMMANDS = [
|
||||
('*IDN?', 'LSCI,MODEL370,370184,05302003'),
|
||||
('SCAN?', '3,1'),
|
||||
('*OPC?', '1'),
|
||||
]
|
||||
|
||||
def earlyInit(self):
|
||||
super().earlyInit()
|
||||
self._data = dict(self.OTHER_COMMANDS)
|
||||
for fmt, v in self.CHANNEL_COMMANDS:
|
||||
for chan in range(1,17):
|
||||
self._data[fmt % chan] = v
|
||||
|
||||
def communicate(self, command):
|
||||
self.comLog('> %s' % command)
|
||||
# simulation part, time independent
|
||||
for channel in range(1,17):
|
||||
_, _, _, _, excoff = self._data['RDGRNG?%d' % channel].split(',')
|
||||
if excoff == '1':
|
||||
self._data['RDGST?%d' % channel] = '6'
|
||||
else:
|
||||
self._data['RDGST?%d' % channel] = '0'
|
||||
|
||||
chunks = command.split(';')
|
||||
reply = []
|
||||
for chunk in chunks:
|
||||
if '?' in chunk:
|
||||
reply.append(self._data[chunk])
|
||||
else:
|
||||
for nqarg in (1,0):
|
||||
if nqarg == 0:
|
||||
qcmd, arg = chunk.split(' ', 1)
|
||||
qcmd += '?'
|
||||
else:
|
||||
qcmd, arg = chunk.split(',', nqarg)
|
||||
qcmd = qcmd.replace(' ', '?', 1)
|
||||
if qcmd in self._data:
|
||||
self._data[qcmd] = arg
|
||||
break
|
||||
reply = ';'.join(reply)
|
||||
self.comLog('< %s' % reply)
|
||||
return reply
|
329
frappy_psi/ls372.py
Normal file
329
frappy_psi/ls372.py
Normal file
@ -0,0 +1,329 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# *****************************************************************************
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Module authors:
|
||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||
# *****************************************************************************
|
||||
"""LakeShore Model 372 resistance channel
|
||||
|
||||
implements autoscan and autorange by software.
|
||||
when the autoscan or autorange button is pressed, the state is toggled,
|
||||
and the hardware mode switched off again.
|
||||
At startup, the configurable default mode is set, independent of
|
||||
the hardware state.
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
import frappy.io
|
||||
from frappy.datatypes import BoolType, EnumType, FloatRange, IntRange
|
||||
from frappy.lib import formatStatusBits
|
||||
from frappy.core import Done, Drivable, Parameter, Property, CommonReadHandler, CommonWriteHandler
|
||||
from frappy.io import HasIO
|
||||
from frappy_psi.channelswitcher import Channel, ChannelSwitcher
|
||||
|
||||
Status = Drivable.Status
|
||||
|
||||
|
||||
STATUS_BIT_LABELS = 'CS_OVL VCM_OVL VMIX_OVL VDIF_OVL R_OVER R_UNDER T_OVER T_UNDER'.split()
|
||||
|
||||
|
||||
def parse1(string):
|
||||
try:
|
||||
return int(string)
|
||||
except ValueError:
|
||||
pass
|
||||
try:
|
||||
return float(string)
|
||||
except ValueError:
|
||||
return string
|
||||
|
||||
|
||||
def parse(reply):
|
||||
return [parse1(s) for s in reply.split(',')]
|
||||
|
||||
|
||||
class StringIO(frappy.io.StringIO):
|
||||
identification = [('*IDN?', 'LSCI,MODEL372,.*')]
|
||||
wait_before = 0.05
|
||||
|
||||
|
||||
class Switcher(HasIO, ChannelSwitcher):
|
||||
value = Parameter(datatype=IntRange(1, 16))
|
||||
target = Parameter(datatype=IntRange(1, 16))
|
||||
use_common_delays = Parameter('use switch_delay and measure_delay instead of the channels pause and dwell',
|
||||
BoolType(), readonly=False, default=False)
|
||||
common_pause = Parameter('pause with common delays', FloatRange(3, 200, unit='s'), readonly=False, default=3)
|
||||
ioClass = StringIO
|
||||
fast_poll = 1
|
||||
_measure_delay = None
|
||||
_switch_delay = None
|
||||
|
||||
def startModule(self, start_events):
|
||||
super().startModule(start_events)
|
||||
# disable unused channels
|
||||
for ch in range(1, 16):
|
||||
if ch not in self._channels:
|
||||
self.communicate('INSET %d,0,0,0,0,0;INSET?%d' % (ch, ch))
|
||||
channelno, autoscan = parse(self.communicate('SCAN?'))
|
||||
if channelno in self._channels and self._channels[channelno].enabled:
|
||||
if not autoscan:
|
||||
return # nothing to do
|
||||
else:
|
||||
channelno = self.next_channel(channelno)
|
||||
if channelno is None:
|
||||
self.status = 'ERROR', 'no enabled channel'
|
||||
return
|
||||
self.communicate('SCAN %d,0;SCAN?' % channelno)
|
||||
|
||||
def doPoll(self):
|
||||
"""poll buttons
|
||||
|
||||
and check autorange during filter time
|
||||
"""
|
||||
super().doPoll()
|
||||
self._channels[self.target]._read_value() # check range or read
|
||||
channelno, autoscan = parse(self.communicate('SCAN?'))
|
||||
if autoscan:
|
||||
# pressed autoscan button: switch off HW autoscan and toggle soft autoscan
|
||||
self.autoscan = not self.autoscan
|
||||
self.communicate('SCAN %d,0;SCAN?' % self.value)
|
||||
if channelno != self.value:
|
||||
# channel changed by keyboard, do not yet return new channel
|
||||
self.write_target(channelno)
|
||||
chan = self._channels.get(channelno)
|
||||
if chan is None:
|
||||
channelno = self.next_channel(channelno)
|
||||
if channelno is None:
|
||||
raise ValueError('no channels enabled')
|
||||
self.write_target(channelno)
|
||||
chan = self._channels.get(self.value)
|
||||
chan.read_autorange()
|
||||
chan.fix_autorange() # check for toggled autorange button
|
||||
return Done
|
||||
|
||||
def write_switch_delay(self, value):
|
||||
self._switch_delay = value
|
||||
return super().write_switch_delay(value)
|
||||
|
||||
def write_measure_delay(self, value):
|
||||
self._measure_delay = value
|
||||
return super().write_measure_delay(value)
|
||||
|
||||
def write_use_common_delays(self, value):
|
||||
if value:
|
||||
# use values from a previous change, instead of
|
||||
# the values from the current channel
|
||||
if self._measure_delay is not None:
|
||||
self.measure_delay = self._measure_delay
|
||||
if self._switch_delay is not None:
|
||||
self.switch_delay = self._switch_delay
|
||||
return value
|
||||
|
||||
def set_delays(self, chan):
|
||||
if self.use_common_delays:
|
||||
if chan.dwell != self.measure_delay:
|
||||
chan.write_dwell(self.measure_delay)
|
||||
if chan.pause != self.common_pause:
|
||||
chan.write_pause(self.common_pause)
|
||||
filter_ = max(0, self.switch_delay - self.common_pause)
|
||||
if chan.filter != filter_:
|
||||
chan.write_filter(filter_)
|
||||
else:
|
||||
# switch_delay and measure_delay is changing with channel
|
||||
self.switch_delay = chan.pause + chan.filter
|
||||
self.measure_delay = chan.dwell
|
||||
|
||||
def set_active_channel(self, chan):
|
||||
self.communicate('SCAN %d,0;SCAN?' % chan.channel)
|
||||
chan._last_range_change = time.monotonic()
|
||||
self.set_delays(chan)
|
||||
|
||||
|
||||
class ResChannel(Channel):
|
||||
"""temperature channel on Lakeshore 372"""
|
||||
|
||||
RES_RANGE = {key: i+1 for i, key in list(
|
||||
enumerate(mag % val for mag in ['%gmOhm', '%gOhm', '%gkOhm', '%gMOhm']
|
||||
for val in [2, 6.32, 20, 63.2, 200, 632]))[:-2]}
|
||||
CUR_RANGE = {key: i + 1 for i, key in list(
|
||||
enumerate(mag % val for mag in ['%gpA', '%gnA', '%guA', '%gmA']
|
||||
for val in [1, 3.16, 10, 31.6, 100, 316]))[:-2]}
|
||||
VOLT_RANGE = {key: i + 1 for i, key in list(
|
||||
enumerate(mag % val for mag in ['%guV', '%gmV']
|
||||
for val in [2, 6.32, 20, 63.2, 200, 632]))}
|
||||
RES_SCALE = [2 * 10 ** (0.5 * i) for i in range(-7, 16)] # RES_SCALE[0] is not used
|
||||
MAX_RNG = len(RES_SCALE) - 1
|
||||
|
||||
channel = Property('the Lakeshore channel', datatype=IntRange(1, 16), export=False)
|
||||
|
||||
value = Parameter(datatype=FloatRange(unit='Ohm'))
|
||||
pollinterval = Parameter(visibility=3, default=1)
|
||||
range = Parameter('reading range', readonly=False,
|
||||
datatype=EnumType(**RES_RANGE))
|
||||
minrange = Parameter('minimum range for software autorange', readonly=False, default=1,
|
||||
datatype=EnumType(**RES_RANGE))
|
||||
autorange = Parameter('autorange', datatype=BoolType(),
|
||||
readonly=False, default=1)
|
||||
iexc = Parameter('current excitation', datatype=EnumType(off=0, **CUR_RANGE), readonly=False)
|
||||
vexc = Parameter('voltage excitation', datatype=EnumType(off=0, **VOLT_RANGE), readonly=False)
|
||||
enabled = Parameter('is this channel enabled?', datatype=BoolType(), readonly=False)
|
||||
pause = Parameter('pause after channel change', datatype=FloatRange(3, 60, unit='s'), readonly=False)
|
||||
dwell = Parameter('dwell time with autoscan', datatype=FloatRange(1, 200, unit='s'), readonly=False)
|
||||
filter = Parameter('filter time', datatype=FloatRange(1, 200, unit='s'), readonly=False)
|
||||
|
||||
_toggle_autorange = 'init'
|
||||
_prev_rdgrng = (1, 1) # last read values for icur and exc
|
||||
_last_range_change = 0
|
||||
rdgrng_params = 'range', 'iexc', 'vexc'
|
||||
inset_params = 'enabled', 'pause', 'dwell'
|
||||
|
||||
def communicate(self, command):
|
||||
return self.switcher.communicate(command)
|
||||
|
||||
def read_status(self):
|
||||
if not self.enabled:
|
||||
return [self.Status.DISABLED, 'disabled']
|
||||
if not self.channel == self.switcher.value == self.switcher.target:
|
||||
return Done
|
||||
result = int(self.communicate('RDGST?%d' % self.channel))
|
||||
result &= 0x37 # mask T_OVER and T_UNDER (change this when implementing temperatures instead of resistivities)
|
||||
statustext = ' '.join(formatStatusBits(result, STATUS_BIT_LABELS))
|
||||
if statustext:
|
||||
return [self.Status.ERROR, statustext]
|
||||
return [self.Status.IDLE, '']
|
||||
|
||||
def _read_value(self):
|
||||
"""read value, without update"""
|
||||
now = time.monotonic()
|
||||
if now + 0.5 < max(self._last_range_change, self.switcher._start_switch) + self.pause:
|
||||
return None
|
||||
result = self.communicate('RDGR?%d' % self.channel)
|
||||
result = float(result)
|
||||
if self.autorange:
|
||||
self.fix_autorange()
|
||||
if now + 0.5 > self._last_range_change + self.pause:
|
||||
rng = int(max(self.minrange, self.range)) # convert from enum to int
|
||||
if self.status[1] == '':
|
||||
if abs(result) > self.RES_SCALE[rng]:
|
||||
if rng < 22:
|
||||
rng += 1
|
||||
else:
|
||||
lim = 0.2
|
||||
while rng > self.minrange and abs(result) < lim * self.RES_SCALE[rng]:
|
||||
rng -= 1
|
||||
lim -= 0.05 # not more than 4 steps at once
|
||||
# effectively: <0.16 %: 4 steps, <1%: 3 steps, <5%: 2 steps, <20%: 1 step
|
||||
elif rng < self.MAX_RNG:
|
||||
rng = min(self.MAX_RNG, rng + 1)
|
||||
if rng != self.range:
|
||||
self.write_range(rng)
|
||||
self._last_range_change = now
|
||||
return result
|
||||
|
||||
def read_value(self):
|
||||
if self.channel == self.switcher.value == self.switcher.target:
|
||||
return self._read_value()
|
||||
return Done # return previous value
|
||||
|
||||
def is_switching(self, now, last_switch, switch_delay):
|
||||
last_switch = max(last_switch, self._last_range_change)
|
||||
if now + 0.5 > last_switch + self.pause:
|
||||
self._read_value() # adjust range only
|
||||
return super().is_switching(now, last_switch, switch_delay)
|
||||
|
||||
@CommonReadHandler(rdgrng_params)
|
||||
def read_rdgrng(self):
|
||||
iscur, exc, rng, autorange, excoff = parse(
|
||||
self.communicate('RDGRNG?%d' % self.channel))
|
||||
self._prev_rdgrng = iscur, exc
|
||||
if autorange: # pressed autorange button
|
||||
if not self._toggle_autorange:
|
||||
self._toggle_autorange = True
|
||||
iexc = 0 if excoff or not iscur else exc
|
||||
vexc = 0 if excoff or iscur else exc
|
||||
if (rng, iexc, vexc) != (self.range, self.iexc, self.vexc):
|
||||
self._last_range_change = time.monotonic()
|
||||
self.range, self.iexc, self.vexc = rng, iexc, vexc
|
||||
|
||||
@CommonWriteHandler(rdgrng_params)
|
||||
def write_rdgrng(self, change):
|
||||
self.read_range() # make sure autorange is handled
|
||||
if 'vexc' in change: # in case vext is changed, do not consider iexc
|
||||
change['iexc'] = 0
|
||||
if change['iexc'] != 0: # we need '!= 0' here, as bool(enum) is always True!
|
||||
iscur = 1
|
||||
exc = change['iexc']
|
||||
excoff = 0
|
||||
elif change['vexc'] != 0: # we need '!= 0' here, as bool(enum) is always True!
|
||||
iscur = 0
|
||||
exc = change['vexc']
|
||||
excoff = 0
|
||||
else:
|
||||
iscur, exc = self._prev_rdgrng # set to last read values
|
||||
excoff = 1
|
||||
rng = change['range']
|
||||
if self.autorange:
|
||||
if rng < self.minrange:
|
||||
rng = self.minrange
|
||||
self.communicate('RDGRNG %d,%d,%d,%d,%d,%d;*OPC?' % (
|
||||
self.channel, iscur, exc, rng, 0, excoff))
|
||||
self.read_range()
|
||||
|
||||
def fix_autorange(self):
|
||||
if self._toggle_autorange:
|
||||
if self._toggle_autorange == 'init':
|
||||
self.write_autorange(True)
|
||||
else:
|
||||
self.write_autorange(not self.autorange)
|
||||
self._toggle_autorange = False
|
||||
|
||||
@CommonReadHandler(inset_params)
|
||||
def read_inset(self):
|
||||
# ignore curve no and temperature coefficient
|
||||
enabled, dwell, pause, _, _ = parse(
|
||||
self.communicate('INSET?%d' % self.channel))
|
||||
self.enabled = enabled
|
||||
self.dwell = dwell
|
||||
self.pause = pause
|
||||
|
||||
@CommonWriteHandler(inset_params)
|
||||
def write_inset(self, change):
|
||||
_, _, _, curve, tempco = parse(
|
||||
self.communicate('INSET?%d' % self.channel))
|
||||
self.enabled, self.dwell, self.pause, _, _ = parse(
|
||||
self.communicate('INSET %d,%d,%d,%d,%d,%d;INSET?%d' % (
|
||||
self.channel, change['enabled'], change['dwell'], change['pause'], curve, tempco,
|
||||
self.channel)))
|
||||
if 'enabled' in change and change['enabled']:
|
||||
# switch to enabled channel
|
||||
self.switcher.write_target(self.channel)
|
||||
elif self.switcher.target == self.channel:
|
||||
self.switcher.set_delays(self)
|
||||
|
||||
def read_filter(self):
|
||||
on, settle, _ = parse(self.communicate('FILTER?%d' % self.channel))
|
||||
return settle if on else 0
|
||||
|
||||
def write_filter(self, value):
|
||||
on = 1 if value else 0
|
||||
value = max(1, value)
|
||||
on, settle, _ = parse(self.communicate(
|
||||
'FILTER %d,%d,%g,80;FILTER?%d' % (self.channel, on, value, self.channel)))
|
||||
if not on:
|
||||
settle = 0
|
||||
return settle
|
381
frappy_psi/magfield.py
Normal file
381
frappy_psi/magfield.py
Normal file
@ -0,0 +1,381 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# *****************************************************************************
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Module authors:
|
||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||
# *****************************************************************************
|
||||
"""generic persistent magnet driver"""
|
||||
|
||||
import time
|
||||
from frappy.core import Drivable, Parameter, Done, IDLE, BUSY, ERROR
|
||||
from frappy.datatypes import FloatRange, EnumType, ArrayOf, TupleOf, StatusType
|
||||
from frappy.features import HasTargetLimits
|
||||
from frappy.errors import ConfigError, ProgrammingError, HardwareError, BadValueError
|
||||
from frappy.lib.enum import Enum
|
||||
from frappy.states import Retry, HasStates, status_code, Start
|
||||
|
||||
UNLIMITED = FloatRange()
|
||||
|
||||
Mode = Enum(
|
||||
DISABLED=0,
|
||||
PERSISTENT=30,
|
||||
DRIVEN=50,
|
||||
)
|
||||
|
||||
Status = Enum(
|
||||
Drivable.Status,
|
||||
PREPARED=150,
|
||||
PREPARING=340,
|
||||
RAMPING=370,
|
||||
STABILIZING=380,
|
||||
FINALIZING=390,
|
||||
)
|
||||
|
||||
OFF = 0
|
||||
ON = 1
|
||||
|
||||
|
||||
class SimpleMagfield(HasStates, HasTargetLimits, Drivable):
|
||||
value = Parameter('magnetic field', datatype=FloatRange(unit='T'))
|
||||
ramp = Parameter(
|
||||
'wanted ramp rate for field', FloatRange(unit='$/min'), readonly=False)
|
||||
# export only when different from ramp:
|
||||
workingramp = Parameter(
|
||||
'effective ramp rate for field', FloatRange(unit='$/min'), export=False)
|
||||
tolerance = Parameter(
|
||||
'tolerance', FloatRange(0, unit='$'), readonly=False, default=0.0002)
|
||||
trained = Parameter(
|
||||
'trained field (positive)',
|
||||
TupleOf(FloatRange(-99, 0, unit='$'), FloatRange(0, unit='$')),
|
||||
readonly=False, default=(0, 0))
|
||||
wait_stable_field = Parameter(
|
||||
'wait time to ensure field is stable', FloatRange(0, unit='s'), readonly=False, default=31)
|
||||
ramp_tmo = Parameter(
|
||||
'timeout for field ramp progress',
|
||||
FloatRange(0, unit='s'), readonly=False, default=30)
|
||||
|
||||
_last_target = None
|
||||
|
||||
def checkProperties(self):
|
||||
dt = self.parameters['target'].datatype
|
||||
max_ = dt.max
|
||||
if max_ == UNLIMITED.max:
|
||||
raise ConfigError('target.max not configured')
|
||||
if dt.min == UNLIMITED.min: # not given: assume bipolar symmetric
|
||||
dt.min = -max_
|
||||
super().checkProperties()
|
||||
|
||||
def stop(self):
|
||||
"""keep field at current value"""
|
||||
# let the state machine do the needed steps to finish
|
||||
self.write_target(self.value)
|
||||
|
||||
def last_target(self):
|
||||
"""get best known last target
|
||||
|
||||
as long as the guessed last target is within tolerance
|
||||
with repsect to the main value, it is used, as in general
|
||||
it has better precision
|
||||
"""
|
||||
last = self._last_target
|
||||
if last is None:
|
||||
try:
|
||||
last = self.setpoint # get read back from HW, if available
|
||||
except Exception:
|
||||
pass
|
||||
if last is None or abs(last - self.value) > self.tolerance:
|
||||
return self.value
|
||||
return last
|
||||
|
||||
def write_target(self, target):
|
||||
self.check_limits(target)
|
||||
self.start_machine(self.start_field_change, target=target)
|
||||
return target
|
||||
|
||||
def init_progress(self, sm, value):
|
||||
sm.prev_point = sm.now, value
|
||||
|
||||
def get_progress(self, sm, value):
|
||||
"""return the time passed for at least one tolerance step"""
|
||||
t, v = sm.prev_point
|
||||
dif = abs(v - value)
|
||||
tdif = sm.now - t
|
||||
if dif > self.tolerance:
|
||||
sm.prev_point = sm.now, value
|
||||
return tdif
|
||||
|
||||
@status_code(BUSY, 'start ramp to target')
|
||||
def start_field_change(self, sm):
|
||||
self.setFastPoll(True, 1.0)
|
||||
return self.start_ramp_to_target
|
||||
|
||||
@status_code(BUSY, 'ramping field')
|
||||
def ramp_to_target(self, sm):
|
||||
if sm.init:
|
||||
self.init_progress(sm, self.value)
|
||||
# Remarks: assume there is a ramp limiting feature
|
||||
if abs(self.value - sm.target) > self.tolerance:
|
||||
if self.get_progress(sm, self.value) > self.ramp_tmo:
|
||||
raise HardwareError('no progress')
|
||||
sm.stabilize_start = None # force reset
|
||||
return Retry
|
||||
sm.stabilize_start = time.time()
|
||||
return self.stabilize_field
|
||||
|
||||
@status_code(BUSY, 'stabilizing field')
|
||||
def stabilize_field(self, sm):
|
||||
if sm.now - sm.stabilize_start < self.wait_stable_field:
|
||||
return Retry
|
||||
return self.final_status()
|
||||
|
||||
def read_workingramp(self):
|
||||
return self.ramp
|
||||
|
||||
|
||||
class Magfield(SimpleMagfield):
|
||||
status = Parameter(datatype=StatusType(Status))
|
||||
mode = Parameter(
|
||||
'persistent mode', EnumType(Mode), readonly=False, initwrite=False, default=Mode.PERSISTENT)
|
||||
switch_heater = Parameter('switch heater', EnumType(off=OFF, on=ON),
|
||||
readonly=False, default=0)
|
||||
current = Parameter(
|
||||
'leads current (in units of field)', FloatRange(unit='$'))
|
||||
# TODO: time_to_target
|
||||
# profile = Parameter(
|
||||
# 'ramp limit table', ArrayOf(TupleOf(FloatRange(unit='$'), FloatRange(unit='$/min'))),
|
||||
# readonly=False)
|
||||
# profile_training = Parameter(
|
||||
# 'ramp limit table when in training',
|
||||
# ArrayOf(TupleOf(FloatRange(unit='$'), FloatRange(unit='$/min'))), readonly=False)
|
||||
# TODO: the following parameters should be changed into properties after tests
|
||||
wait_switch_on = Parameter(
|
||||
'wait time to ensure switch is on', FloatRange(0, unit='s'), readonly=False, default=61)
|
||||
wait_switch_off = Parameter(
|
||||
'wait time to ensure switch is off', FloatRange(0, unit='s'), readonly=False, default=61)
|
||||
wait_stable_leads = Parameter(
|
||||
'wait time to ensure current is stable', FloatRange(0, unit='s'), readonly=False, default=6)
|
||||
persistent_limit = Parameter(
|
||||
'above this limit, lead currents are not driven to 0',
|
||||
FloatRange(0, unit='$'), readonly=False, default=99)
|
||||
leads_ramp_tmo = Parameter(
|
||||
'timeout for leads ramp progress',
|
||||
FloatRange(0, unit='s'), readonly=False, default=30)
|
||||
init_persistency = True
|
||||
switch_on_time = None
|
||||
switch_off_time = None
|
||||
|
||||
def doPoll(self):
|
||||
if self.init_persistency:
|
||||
if self.read_switch_heater() and self.mode != Mode.DRIVEN:
|
||||
self.start_machine(self.go_persistent_soon, mode=self.mode)
|
||||
self.init_persistency = False
|
||||
super().doPoll()
|
||||
|
||||
def initModule(self):
|
||||
super().initModule()
|
||||
self.registerCallbacks(self) # for update_switch_heater
|
||||
|
||||
def write_mode(self, value):
|
||||
self.init_persistency = False
|
||||
target = self.last_target()
|
||||
func = self.start_field_change
|
||||
if value == Mode.DISABLED:
|
||||
target = 0
|
||||
if abs(self.value) < self.tolerance:
|
||||
func = self.start_switch_off
|
||||
elif value == Mode.PERSISTENT:
|
||||
func = self.start_switch_off
|
||||
self.target = target
|
||||
self.start_machine(func, target=target, mode=value)
|
||||
return value
|
||||
|
||||
def write_target(self, target):
|
||||
self.init_persistency = False
|
||||
if self.mode == Mode.DISABLED:
|
||||
if target == 0:
|
||||
return 0
|
||||
self.log.info('raise error %r', target)
|
||||
raise BadValueError('disabled')
|
||||
self.check_limits(target)
|
||||
self.start_machine(self.start_field_change, target=target, mode=self.mode)
|
||||
return target
|
||||
|
||||
def on_error(self, sm): # sm is short for statemachine
|
||||
if self.switch_heater == ON:
|
||||
self.read_value()
|
||||
if sm.mode != Mode.DRIVEN:
|
||||
self.log.warning('turn switch heater off')
|
||||
self.write_switch_heater(OFF)
|
||||
return super().on_error(sm)
|
||||
|
||||
@status_code(Status.WARN)
|
||||
def go_persistent_soon(self, sm):
|
||||
if sm.delta(60):
|
||||
self.target = sm.target = self.last_target()
|
||||
return self.start_field_change
|
||||
return Retry
|
||||
|
||||
@status_code(Status.PREPARING)
|
||||
def start_field_change(self, sm):
|
||||
self.setFastPoll(True, 1.0)
|
||||
if (sm.target == self.last_target() and
|
||||
abs(sm.target - self.value) <= self.tolerance and
|
||||
abs(self.current - self.value) < self.tolerance and
|
||||
(self.mode != Mode.DRIVEN or self.switch_heater == ON)): # short cut
|
||||
return self.check_switch_off
|
||||
if self.switch_heater == ON:
|
||||
return self.start_switch_on
|
||||
return self.start_ramp_to_field
|
||||
|
||||
@status_code(Status.PREPARING)
|
||||
def start_ramp_to_field(self, sm):
|
||||
"""start ramping current to persistent field
|
||||
|
||||
initiate ramp to persistent field (with corresponding ramp rate)
|
||||
the implementation should return ramp_to_field
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@status_code(Status.PREPARING, 'ramp leads to match field')
|
||||
def ramp_to_field(self, sm):
|
||||
if sm.init:
|
||||
sm.stabilize_start = 0 # in case current is already at field
|
||||
self.init_progress(sm, self.current)
|
||||
dif = abs(self.current - self.value)
|
||||
if dif > self.tolerance:
|
||||
tdif = self.get_progress(sm, self.current)
|
||||
if tdif > self.leads_ramp_tmo:
|
||||
raise HardwareError('no progress')
|
||||
sm.stabilize_start = None # force reset
|
||||
return Retry
|
||||
if sm.stabilize_start is None:
|
||||
sm.stabilize_start = sm.now
|
||||
return self.stabilize_current
|
||||
|
||||
@status_code(Status.PREPARING)
|
||||
def stabilize_current(self, sm):
|
||||
if sm.now - sm.stabilize_start < self.wait_stable_leads:
|
||||
return Retry
|
||||
return self.start_switch_on
|
||||
|
||||
def update_switch_heater(self, value):
|
||||
"""is called whenever switch heater was changed"""
|
||||
if value == 0:
|
||||
if self.switch_off_time is None:
|
||||
self.log.info('restart switch_off_time')
|
||||
self.switch_off_time = time.time()
|
||||
self.switch_on_time = None
|
||||
else:
|
||||
if self.switch_on_time is None:
|
||||
self.log.info('restart switch_on_time')
|
||||
self.switch_on_time = time.time()
|
||||
self.switch_off_time = None
|
||||
|
||||
@status_code(Status.PREPARING)
|
||||
def start_switch_on(self, sm):
|
||||
if (sm.target == self.last_target() and
|
||||
abs(sm.target - self.value) <= self.tolerance): # short cut
|
||||
return self.check_switch_off
|
||||
if self.read_switch_heater() == OFF:
|
||||
try:
|
||||
self.write_switch_heater(ON)
|
||||
except Exception as e:
|
||||
self.log.warning('write_switch_heater %r', e)
|
||||
return Retry
|
||||
return self.wait_for_switch_on
|
||||
|
||||
@status_code(Status.PREPARING)
|
||||
def wait_for_switch_on(self, sm):
|
||||
if sm.now - self.switch_on_time < self.wait_switch_on:
|
||||
if sm.delta(10):
|
||||
self.log.info('waited for %g sec', sm.now - self.switch_on_time)
|
||||
return Retry
|
||||
self._last_target = sm.target
|
||||
return self.start_ramp_to_target
|
||||
|
||||
@status_code(Status.RAMPING)
|
||||
def start_ramp_to_target(self, sm):
|
||||
"""start ramping current to target field
|
||||
|
||||
initiate ramp to target
|
||||
the implementation should return ramp_to_target
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@status_code(Status.RAMPING)
|
||||
def ramp_to_target(self, sm):
|
||||
dif = abs(self.value - sm.target)
|
||||
if sm.init:
|
||||
sm.stabilize_start = 0 # in case current is already at target
|
||||
self.init_progress(sm, self.value)
|
||||
if dif > self.tolerance:
|
||||
sm.stabilize_start = sm.now
|
||||
tdif = self.get_progress(sm, self.value)
|
||||
if tdif > self.workingramp / self.tolerance * 60 + self.ramp_tmo:
|
||||
self.log.warn('no progress')
|
||||
raise HardwareError('no progress')
|
||||
sm.stabilize_start = None
|
||||
return Retry
|
||||
if sm.stabilize_start is None:
|
||||
sm.stabilize_start = sm.now
|
||||
return self.stabilize_field
|
||||
|
||||
@status_code(Status.STABILIZING)
|
||||
def stabilize_field(self, sm):
|
||||
if sm.now < sm.stabilize_start + self.wait_stable_field:
|
||||
return Retry
|
||||
return self.check_switch_off
|
||||
|
||||
def check_switch_off(self, sm):
|
||||
if sm.mode == Mode.DRIVEN:
|
||||
return self.final_status(Status.PREPARED, 'driven')
|
||||
return self.start_switch_off
|
||||
|
||||
@status_code(Status.FINALIZING)
|
||||
def start_switch_off(self, sm):
|
||||
if self.switch_heater == ON:
|
||||
self.write_switch_heater(OFF)
|
||||
return self.wait_for_switch_off
|
||||
|
||||
@status_code(Status.FINALIZING)
|
||||
def wait_for_switch_off(self, sm):
|
||||
if sm.now - self.switch_off_time < self.wait_switch_off:
|
||||
return Retry
|
||||
if abs(self.value) > self.persistent_limit:
|
||||
return self.final_status(Status.IDLE, 'leads current at field, switch off')
|
||||
return self.start_ramp_to_zero
|
||||
|
||||
@status_code(Status.FINALIZING)
|
||||
def start_ramp_to_zero(self, sm):
|
||||
"""start ramping current to zero
|
||||
|
||||
initiate ramp to zero (with corresponding ramp rate)
|
||||
the implementation should return ramp_to_zero
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@status_code(Status.FINALIZING)
|
||||
def ramp_to_zero(self, sm):
|
||||
"""ramp field to zero"""
|
||||
if sm.init:
|
||||
self.init_progress(sm, self.current)
|
||||
if abs(self.current) > self.tolerance:
|
||||
if self.get_progress(sm, self.value) > self.leads_ramp_tmo:
|
||||
raise HardwareError('no progress')
|
||||
return Retry
|
||||
if sm.mode == Mode.DISABLED and abs(self.value) < self.tolerance:
|
||||
return self.final_status(Status.DISABLED, 'disabled')
|
||||
return self.final_status(Status.IDLE, 'persistent mode')
|
254
frappy_psi/phytron.py
Normal file
254
frappy_psi/phytron.py
Normal file
@ -0,0 +1,254 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# *****************************************************************************
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Module authors:
|
||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||
#
|
||||
# *****************************************************************************
|
||||
|
||||
"""driver for phytron motors"""
|
||||
|
||||
from frappy.core import Done, Command, EnumType, FloatRange, IntRange, \
|
||||
HasIO, Parameter, Property, Drivable, PersistentMixin, PersistentParam, \
|
||||
StringIO, StringType, TupleOf
|
||||
from frappy.errors import CommunicationFailedError, HardwareError, BadValueError
|
||||
from frappy.lib import clamp
|
||||
|
||||
|
||||
class PhytronIO(StringIO):
|
||||
end_of_line = '\x03' # ETX
|
||||
timeout = 0.5
|
||||
identification = [('0IVR', 'MCC Minilog .*')]
|
||||
|
||||
def communicate(self, command):
|
||||
ntry = 5
|
||||
warn = None
|
||||
for itry in range(ntry):
|
||||
try:
|
||||
_, _, reply = super().communicate('\x02' + command).partition('\x02')
|
||||
if reply[0] == '\x06': # ACK
|
||||
break
|
||||
raise CommunicationFailedError('missing ACK %r (cmd: %r)'
|
||||
% (reply, command))
|
||||
except Exception as e:
|
||||
if itry < ntry - 1:
|
||||
warn = e
|
||||
else:
|
||||
raise
|
||||
if warn:
|
||||
self.log.warning('needed %d retries after %r', itry, warn)
|
||||
return reply[1:]
|
||||
|
||||
|
||||
class Motor(PersistentMixin, HasIO, Drivable):
|
||||
axis = Property('motor axis X or Y', StringType(), default='X')
|
||||
address = Property('address', IntRange(0, 15), default=0)
|
||||
|
||||
encoder_mode = Parameter('how to treat the encoder', EnumType('encoder', NO=0, READ=1, CHECK=2),
|
||||
default=1, readonly=False)
|
||||
value = Parameter('angle', FloatRange(unit='deg'))
|
||||
target = Parameter('target angle', FloatRange(unit='deg'), readonly=False)
|
||||
speed = Parameter('', FloatRange(0, 20, unit='deg/s'), readonly=False)
|
||||
accel = Parameter('', FloatRange(2, 250, unit='deg/s/s'), readonly=False)
|
||||
encoder_tolerance = Parameter('', FloatRange(unit='deg'), readonly=False, default=0.01)
|
||||
offset = PersistentParam('', FloatRange(unit='deg'), readonly=False, default=0)
|
||||
sign = PersistentParam('', IntRange(-1,1), readonly=False, default=1)
|
||||
encoder = Parameter('encoder reading', FloatRange(unit='deg'))
|
||||
backlash = Parameter("""backlash compensation\n
|
||||
offset for always approaching from the same side""",
|
||||
FloatRange(unit='deg'), readonly=False, default=0)
|
||||
abslimits = Parameter('abs limits (raw values)', default=(0, 0),
|
||||
datatype=TupleOf(FloatRange(unit='deg'), FloatRange(unit='deg')))
|
||||
userlimits = PersistentParam('user limits', readonly=False, default=(0, 0), initwrite=True,
|
||||
datatype=TupleOf(FloatRange(unit='deg'), FloatRange(unit='deg')))
|
||||
|
||||
ioClass = PhytronIO
|
||||
fast_poll = 0.1
|
||||
_backlash_pending = False
|
||||
_mismatch_count = 0
|
||||
_rawlimits = None
|
||||
_step_size = None # degree / step
|
||||
|
||||
def earlyInit(self):
|
||||
super().earlyInit()
|
||||
if self.abslimits == (0, 0):
|
||||
self.abslimits = -9e99, 9e99
|
||||
if self.userlimits == (0, 0):
|
||||
self._rawlimits = self.abslimits
|
||||
self.read_userlimits()
|
||||
self.loadParameters()
|
||||
|
||||
def get(self, cmd):
|
||||
return self.communicate('%x%s%s' % (self.address, self.axis, cmd))
|
||||
|
||||
def set(self, cmd, value):
|
||||
# make sure e format is not used, max 8 characters
|
||||
strvalue = '%.6g' % value
|
||||
if 'e' in strvalue:
|
||||
if abs(value) <= 1: # very small number
|
||||
strvalue = '%.7f' % value
|
||||
elif abs(value) < 99999999: # big number
|
||||
strvalue = '%.0f' % value
|
||||
else:
|
||||
raise ValueError('number (%g) must not have more than 8 digits' % value)
|
||||
self.communicate('%x%s%s%s' % (self.address, self.axis, cmd, strvalue))
|
||||
|
||||
def set_get(self, cmd, value, query):
|
||||
self.set(cmd, value)
|
||||
return self.get(query)
|
||||
|
||||
def read_value(self):
|
||||
prev_enc = self.encoder
|
||||
pos = float(self.get('P20R')) * self.sign - self.offset
|
||||
if self.encoder_mode != 'NO':
|
||||
enc = self.read_encoder()
|
||||
else:
|
||||
enc = pos
|
||||
status = self.communicate('%xSE' % self.address)
|
||||
status = status[0:4] if self.axis == 'X' else status[4:8]
|
||||
self.log.debug('run %s enc %s end %s', status[1], status[2], status[3])
|
||||
status = self.get('=H')
|
||||
if status == 'N':
|
||||
if self.encoder_mode == 'CHECK':
|
||||
e1, e2 = sorted((prev_enc, enc))
|
||||
if e1 - self.encoder_tolerance <= pos <= e2 + self.encoder_tolerance:
|
||||
self.status = self.Status.BUSY, 'driving'
|
||||
else:
|
||||
self.log.error('encoder lag: %g not within %g..%g',
|
||||
pos, e1, e2)
|
||||
self.get('S') # stop
|
||||
self.status = self.Status.ERROR, 'encoder lag error'
|
||||
self.setFastPoll(False)
|
||||
else:
|
||||
self.status = self.Status.BUSY, 'driving'
|
||||
else:
|
||||
if self._backlash_pending:
|
||||
# drive to real target
|
||||
self.set('A', self.sign * (self.target + self.offset))
|
||||
self._backlash_pending = False
|
||||
return pos
|
||||
if (self.encoder_mode == 'CHECK' and
|
||||
abs(enc - pos) > self.encoder_tolerance):
|
||||
if self._mismatch_count < 2:
|
||||
self._mismatch_count += 1
|
||||
else:
|
||||
self.log.error('encoder mismatch: abs(%g - %g) < %g',
|
||||
enc, pos, self.encoder_tolerance)
|
||||
self.status = self.Status.ERROR, 'encoder does not match pos'
|
||||
else:
|
||||
self._mismatch_count = 0
|
||||
self.status = self.Status.IDLE, ''
|
||||
self.setFastPoll(False)
|
||||
return pos
|
||||
|
||||
def read_encoder(self):
|
||||
if self.encoder_mode == 'NO':
|
||||
return self.value
|
||||
return float(self.get('P22R')) * self.sign - self.offset
|
||||
|
||||
def write_sign(self, value):
|
||||
self.sign = value
|
||||
self.saveParameters()
|
||||
return Done
|
||||
|
||||
def get_step_size(self):
|
||||
self._step_size = float(self.get('P03R'))
|
||||
|
||||
def read_speed(self):
|
||||
if self._step_size is None:
|
||||
# avoid repeatedly reading step size, as this is polled and will not change
|
||||
self.get_step_size()
|
||||
return float(self.get('P14R')) * self._step_size
|
||||
|
||||
def write_speed(self, value):
|
||||
self.get_step_size() # read step size anyway, it does not harm
|
||||
return float(self.set_get('P14S', round(value / self._step_size), 'P14R')) * self._step_size
|
||||
|
||||
def read_accel(self):
|
||||
if not self._step_size:
|
||||
self.get_step_size()
|
||||
return float(self.get('P15R')) * self._step_size
|
||||
|
||||
def write_accel(self, value):
|
||||
self.get_step_size()
|
||||
return float(self.set_get('P15S', round(value / self._step_size), 'P15R')) * self._step_size
|
||||
|
||||
def _check_limits(self, *values):
|
||||
for name, (mn, mx) in ('user', self._rawlimits), ('abs', self.abslimits):
|
||||
mn -= self.offset
|
||||
mx -= self.offset
|
||||
for v in values:
|
||||
if not mn <= v <= mx:
|
||||
raise BadValueError('%s limits violation: %g <= %g <= %g' % (name, mn, v, mx))
|
||||
v += self.offset
|
||||
|
||||
def write_target(self, value):
|
||||
if self.status[0] == self.Status.ERROR:
|
||||
raise HardwareError('need reset')
|
||||
self.status = self.Status.BUSY, 'changed target'
|
||||
self._check_limits(value, value + self.backlash)
|
||||
if self.backlash:
|
||||
# drive first to target + backlash
|
||||
# we do not optimize when already driving from the right side
|
||||
self._backlash_pending = True
|
||||
self.set('A', self.sign * (value + self.offset + self.backlash))
|
||||
else:
|
||||
self.set('A', self.sign * (value + self.offset))
|
||||
self.setFastPoll(True, self.fast_poll)
|
||||
return value
|
||||
|
||||
def read_userlimits(self):
|
||||
return self._rawlimits[0] - self.offset, self._rawlimits[1] - self.offset
|
||||
|
||||
def write_userlimits(self, value):
|
||||
self._rawlimits = [clamp(self.abslimits[0], v + self.offset, self.abslimits[1]) for v in value]
|
||||
value = self.read_userlimits()
|
||||
self.saveParameters()
|
||||
return value
|
||||
|
||||
def write_offset(self, value):
|
||||
self.offset = value
|
||||
self.read_userlimits()
|
||||
self.saveParameters()
|
||||
return Done
|
||||
|
||||
def stop(self):
|
||||
self.get('S')
|
||||
|
||||
@Command
|
||||
def reset(self):
|
||||
"""Reset error, set position to encoder"""
|
||||
self.read_value()
|
||||
if self.status[0] == self.Status.ERROR:
|
||||
enc = self.encoder + self.offset
|
||||
pos = self.value + self.offset
|
||||
if abs(enc - pos) > self.encoder_tolerance:
|
||||
if enc < 0:
|
||||
# assume we have a rotation (not a linear motor)
|
||||
while enc < 0:
|
||||
self.offset += 360
|
||||
enc += 360
|
||||
self.set('P22S', enc * self.sign)
|
||||
self.saveParameters()
|
||||
self.set('P20S', enc * self.sign) # set pos to encoder
|
||||
self.read_value()
|
||||
# self.status = self.Status.IDLE, ''
|
||||
|
||||
# TODO:
|
||||
# '=E' electronics status
|
||||
# '=I+' / '=I-': limit switches
|
||||
# use P37 to determine if restarted
|
704
frappy_psi/sea.py
Normal file
704
frappy_psi/sea.py
Normal file
@ -0,0 +1,704 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# *****************************************************************************
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Module authors:
|
||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||
# *****************************************************************************
|
||||
"""generic SEA driver
|
||||
|
||||
a object or subobject in sea may be assigned to a SECoP module
|
||||
|
||||
Examples:
|
||||
|
||||
SECoP SEA hipadaba path mod.obj mod.sub par.sub mod.path
|
||||
-------------------------------------------------------------------------------
|
||||
tt:maxwait tt /tt/maxwait tt maxwait /tt
|
||||
tt:ramp tt set/ramp /tt/set/ramp tt set/ramp /tt
|
||||
t1:raw tt t1/raw /tt/t1/raw tt t1 raw /tt/t1
|
||||
rx:bla rx bla /some/rx_a/bla rx bla /some/rx_a
|
||||
"""
|
||||
|
||||
import json
|
||||
import threading
|
||||
import time
|
||||
import os
|
||||
from os.path import expanduser, join, exists
|
||||
|
||||
from frappy.client import ProxyClient
|
||||
from frappy.datatypes import ArrayOf, BoolType, \
|
||||
EnumType, FloatRange, IntRange, StringType
|
||||
from frappy.errors import ConfigError, HardwareError, secop_error, NoSuchModuleError, \
|
||||
CommunicationFailedError
|
||||
from frappy.lib import generalConfig, mkthread, formatExtendedStack
|
||||
from frappy.lib.asynconn import AsynConn, ConnectionClosed
|
||||
from frappy.modules import Attached, Command, Done, Drivable, \
|
||||
Module, Parameter, Property, Readable, Writable
|
||||
from frappy.protocol.dispatcher import make_update
|
||||
|
||||
|
||||
CFG_HEADER = """Node(
|
||||
description = '''%(nodedescr)s''',
|
||||
id = %(config)s.sea.psi.ch,
|
||||
)
|
||||
Mod(%(seaconn)r,
|
||||
'frappy_psi.sea.SeaClient',
|
||||
'%(service)s sea connection for %(config)s',
|
||||
config = %(config)r,
|
||||
service = %(service)r,
|
||||
)
|
||||
"""
|
||||
|
||||
CFG_MODULE = """Mod(%(module)r,
|
||||
'frappy_psi.sea.%(modcls)s',
|
||||
io = %(seaconn)r,
|
||||
sea_object = %(module)r,
|
||||
)
|
||||
"""
|
||||
|
||||
SERVICE_NAMES = {
|
||||
'config': 'main',
|
||||
'stick': 'stick',
|
||||
'addon': 'addons',
|
||||
}
|
||||
|
||||
SEA_DIR = expanduser('~/sea')
|
||||
for confdir in generalConfig.confdir.split(os.pathsep):
|
||||
seaconfdir = join(confdir, 'sea')
|
||||
if exists(seaconfdir):
|
||||
break
|
||||
else:
|
||||
seaconfdir = os.environ.get('FRAPPY_SEA_DIR')
|
||||
|
||||
|
||||
def get_sea_port(instance):
|
||||
for filename in ('sea_%s.tcl' % instance, 'sea.tcl'):
|
||||
try:
|
||||
with open(join(SEA_DIR, filename)) as f:
|
||||
for line in f:
|
||||
linesplit = line.split()
|
||||
if len(linesplit) == 3:
|
||||
_, var, value = line.split()
|
||||
if var == 'serverport':
|
||||
return value
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
class SeaClient(ProxyClient, Module):
|
||||
"""connection to SEA"""
|
||||
|
||||
uri = Parameter('hostname:portnumber', datatype=StringType(), default='localhost:5000')
|
||||
timeout = Parameter('timeout', datatype=FloatRange(0), default=10)
|
||||
config = Property("""needed SEA configuration, space separated
|
||||
|
||||
Example: "ori4.config ori4.stick"
|
||||
""", StringType(), default='')
|
||||
service = Property("main/stick/addons", StringType(), default='')
|
||||
visibility = 'expert'
|
||||
default_json_file = {}
|
||||
_connect_thread = None
|
||||
_service_manager = None
|
||||
_instance = None
|
||||
|
||||
def __init__(self, name, log, opts, srv):
|
||||
instance = srv.node_cfg['name'].rsplit('_', 1)[0]
|
||||
if 'uri' not in opts:
|
||||
self._instance = instance
|
||||
port = get_sea_port(instance)
|
||||
if port is None:
|
||||
raise ConfigError('missing sea port for %s' % instance)
|
||||
opts['uri'] = 'tcp://localhost:%s' % port
|
||||
self.objects = set()
|
||||
self.shutdown = False
|
||||
self.path2param = {}
|
||||
self._write_lock = threading.Lock()
|
||||
config = opts.get('config')
|
||||
if config:
|
||||
self.default_json_file[name] = config.split()[0] + '.json'
|
||||
self.syncio = None
|
||||
self.asynio = None
|
||||
ProxyClient.__init__(self)
|
||||
Module.__init__(self, name, log, opts, srv)
|
||||
|
||||
def register_obj(self, module, obj):
|
||||
self.objects.add(obj)
|
||||
for k, v in module.path2param.items():
|
||||
self.path2param.setdefault(k, []).extend(v)
|
||||
self.register_callback(module.name, module.updateEvent)
|
||||
|
||||
def startModule(self, start_events):
|
||||
super().startModule(start_events)
|
||||
self._connect_thread = mkthread(self._connect, start_events.get_trigger())
|
||||
|
||||
def _connect(self, started_callback):
|
||||
if self._instance:
|
||||
if not self._service_manager:
|
||||
from servicemanager import SeaManager
|
||||
self._service_manager = SeaManager()
|
||||
self._service_manager.do_start(self._instance)
|
||||
if '//' not in self.uri:
|
||||
self.uri = 'tcp://' + self.uri
|
||||
self.asynio = AsynConn(self.uri)
|
||||
# print('CONNECT', self.uri, self.asynio)
|
||||
# print(formatExtendedStack())
|
||||
reply = self.asynio.readline()
|
||||
if reply != b'OK':
|
||||
raise CommunicationFailedError('reply %r should be "OK"' % reply)
|
||||
for _ in range(2):
|
||||
self.asynio.writeline(b'Spy 007')
|
||||
reply = self.asynio.readline()
|
||||
if reply == b'Login OK':
|
||||
break
|
||||
else:
|
||||
raise CommunicationFailedError('reply %r should be "Login OK"' % reply)
|
||||
self.request('frappy_config %s %s' % (self.service, self.config))
|
||||
|
||||
# frappy_async_client switches to the json protocol (better for updates)
|
||||
self.asynio.writeline(b'frappy_async_client')
|
||||
self.asynio.writeline(('get_all_param ' + ' '.join(self.objects)).encode())
|
||||
self._connect_thread = None
|
||||
mkthread(self._rxthread, started_callback)
|
||||
|
||||
def request(self, command, quiet=False):
|
||||
"""send a request and wait for reply"""
|
||||
with self._write_lock:
|
||||
if not self.syncio or not self.syncio.connection:
|
||||
if not self.asynio or not self.asynio.connection:
|
||||
try:
|
||||
self._connect_thread.join()
|
||||
except AttributeError:
|
||||
pass
|
||||
self._connect(None)
|
||||
self.syncio = AsynConn(self.uri)
|
||||
# print('SYNCIO', self.uri)
|
||||
assert self.syncio.readline() == b'OK'
|
||||
self.syncio.writeline(b'seauser seaser')
|
||||
assert self.syncio.readline() == b'Login OK'
|
||||
print('connected to %s' % self.uri)
|
||||
self.syncio.flush_recv()
|
||||
# print('> %s' % command)
|
||||
ft = 'fulltransAct' if quiet else 'fulltransact'
|
||||
self.syncio.writeline(('%s %s' % (ft, command)).encode())
|
||||
result = None
|
||||
deadline = time.time() + 10
|
||||
while time.time() < deadline:
|
||||
try:
|
||||
reply = self.syncio.readline()
|
||||
if reply is None:
|
||||
continue
|
||||
except ConnectionClosed:
|
||||
break
|
||||
reply = reply.decode()
|
||||
# print('< %s' % reply)
|
||||
if reply.startswith('TRANSACTIONSTART'):
|
||||
result = []
|
||||
continue
|
||||
if reply == 'TRANSACTIONFINISHED':
|
||||
if result is None:
|
||||
print('missing TRANSACTIONSTART on: %s' % command)
|
||||
return ''
|
||||
if not result:
|
||||
return ''
|
||||
return '\n'.join(result)
|
||||
if result is None:
|
||||
print('swallow: %s' % reply)
|
||||
continue
|
||||
if not result:
|
||||
result = [reply.split('=', 1)[-1]]
|
||||
else:
|
||||
result.append(reply)
|
||||
raise TimeoutError('no response within 10s')
|
||||
|
||||
def _rxthread(self, started_callback):
|
||||
while not self.shutdown:
|
||||
try:
|
||||
reply = self.asynio.readline()
|
||||
if reply is None:
|
||||
continue
|
||||
except ConnectionClosed:
|
||||
break
|
||||
try:
|
||||
msg = json.loads(reply)
|
||||
except Exception as e:
|
||||
print(repr(e), reply)
|
||||
continue
|
||||
if isinstance(msg, str):
|
||||
if msg.startswith('_E '):
|
||||
try:
|
||||
_, path, readerror = msg.split(None, 2)
|
||||
except ValueError:
|
||||
continue
|
||||
else:
|
||||
continue
|
||||
data = {'%s.geterror' % path: readerror.replace('ERROR: ', '')}
|
||||
obj = None
|
||||
flag = 'hdbevent'
|
||||
else:
|
||||
obj = msg['object']
|
||||
flag = msg['flag']
|
||||
data = msg['data']
|
||||
if flag == 'finish' and obj == 'get_all_param':
|
||||
# first updates have finished
|
||||
if started_callback:
|
||||
started_callback()
|
||||
started_callback = None
|
||||
continue
|
||||
if flag != 'hdbevent':
|
||||
if obj not in ('frappy_async_client', 'get_all_param'):
|
||||
print('SKIP', msg)
|
||||
continue
|
||||
if not data:
|
||||
continue
|
||||
if not isinstance(data, dict):
|
||||
print('what means %r' % msg)
|
||||
continue
|
||||
now = time.time()
|
||||
for path, value in data.items():
|
||||
readerror = None
|
||||
if path.endswith('.geterror'):
|
||||
if value:
|
||||
readerror = HardwareError(value)
|
||||
path = path.rsplit('.', 1)[0]
|
||||
value = None
|
||||
mplist = self.path2param.get(path)
|
||||
if mplist is None:
|
||||
if path.startswith('/device'):
|
||||
if path == '/device/changetime':
|
||||
result = self.request('check_config %s %s' % (self.service, self.config))
|
||||
if result == '1':
|
||||
self.asynio.writeline(('get_all_param ' + ' '.join(self.objects)).encode())
|
||||
else:
|
||||
self.DISPATCHER.shutdown()
|
||||
elif path.startswith('/device/frappy_%s' % self.service) and value == '':
|
||||
self.DISPATCHER.shutdown()
|
||||
else:
|
||||
for module, param in mplist:
|
||||
oldv, oldt, oldr = self.cache.get((module, param), [None, None, None])
|
||||
if value is None:
|
||||
value = oldv
|
||||
if value != oldv or str(readerror) != str(oldr) or abs(now - oldt) > 60:
|
||||
# do not update unchanged values within 60 sec
|
||||
self.updateValue(module, param, value, now, readerror)
|
||||
|
||||
@Command(StringType(), result=StringType())
|
||||
def communicate(self, command):
|
||||
"""send a command to SEA"""
|
||||
reply = self.request(command)
|
||||
return reply
|
||||
|
||||
@Command(StringType(), result=StringType())
|
||||
def query(self, cmd, quiet=False):
|
||||
"""a request checking for errors and accepting 0 or 1 line as result"""
|
||||
errors = []
|
||||
reply = None
|
||||
for line in self.request(cmd, quiet).split('\n'):
|
||||
if line.strip().startswith('ERROR:'):
|
||||
errors.append(line[6:].strip())
|
||||
elif reply is None:
|
||||
reply = line.strip()
|
||||
else:
|
||||
self.log.info('SEA: superfluous reply %r to %r', reply, cmd)
|
||||
if errors:
|
||||
raise HardwareError('; '.join(errors))
|
||||
return reply
|
||||
|
||||
|
||||
class SeaConfigCreator(SeaClient):
|
||||
def startModule(self, started_callback):
|
||||
"""save objects (and sub-objects) description and exit"""
|
||||
self._connect(None)
|
||||
reply = self.request('describe_all')
|
||||
reply = ''.join('' if line.startswith('WARNING') else line for line in reply.split('\n'))
|
||||
description, reply = json.loads(reply)
|
||||
modules = {}
|
||||
modcls = {}
|
||||
for filename, obj, descr in reply:
|
||||
if filename not in modules:
|
||||
modules[filename] = {}
|
||||
if descr['params'][0].get('cmd', '').startswith('run '):
|
||||
modcls[obj] = 'SeaDrivable'
|
||||
elif not descr['params'][0].get('readonly', True):
|
||||
modcls[obj] = 'SeaWritable'
|
||||
else:
|
||||
modcls[obj] = 'SeaReadable'
|
||||
modules.setdefault(filename, {})[obj] = descr
|
||||
|
||||
result = []
|
||||
for filename, descr in modules.items():
|
||||
stripped, _, ext = filename.rpartition('.')
|
||||
service = SERVICE_NAMES[ext]
|
||||
seaconn = 'sea_' + service
|
||||
cfgfile = join(seaconfdir, stripped + '_cfg.py')
|
||||
with open(cfgfile, 'w') as fp:
|
||||
fp.write(CFG_HEADER % dict(config=filename, seaconn=seaconn, service=service,
|
||||
nodedescr=description.get(filename, filename)))
|
||||
for obj in descr:
|
||||
fp.write(CFG_MODULE % dict(modcls=modcls[obj], module=obj, seaconn=seaconn))
|
||||
content = json.dumps(descr).replace('}, {', '},\n{').replace('[{', '[\n{').replace('}]}, ', '}]},\n\n')
|
||||
result.append('%s\n' % cfgfile)
|
||||
with open(join(seaconfdir, filename + '.json'), 'w') as fp:
|
||||
fp.write(content + '\n')
|
||||
result.append('%s: %s' % (filename, ','.join(n for n in descr)))
|
||||
raise SystemExit('; '.join(result))
|
||||
|
||||
@Command(StringType(), result=StringType())
|
||||
def query(self, cmd):
|
||||
"""a request checking for errors and accepting 0 or 1 line as result"""
|
||||
errors = []
|
||||
reply = None
|
||||
for line in self.request(cmd).split('\n'):
|
||||
if line.strip().startswith('ERROR:'):
|
||||
errors.append(line[6:].strip())
|
||||
elif reply is None:
|
||||
reply = line.strip()
|
||||
else:
|
||||
self.log.info('SEA: superfluous reply %r to %r', reply, cmd)
|
||||
if errors:
|
||||
raise HardwareError('; '.join(errors))
|
||||
return reply
|
||||
|
||||
|
||||
SEA_TO_SECOPTYPE = {
|
||||
'float': FloatRange(),
|
||||
'text': StringType(),
|
||||
'int': IntRange(-1 << 63, 1 << 63 - 1),
|
||||
'bool': BoolType(),
|
||||
'none': None,
|
||||
'floatvarar': ArrayOf(FloatRange(), 0, 400), # 400 is the current limit for proper notify events in SEA
|
||||
}
|
||||
|
||||
|
||||
def get_datatype(paramdesc):
|
||||
typ = paramdesc['type']
|
||||
result = SEA_TO_SECOPTYPE.get(typ, False)
|
||||
if result is not False: # general case
|
||||
return result
|
||||
# special cases
|
||||
if typ == 'enum':
|
||||
return EnumType(paramdesc['enum'])
|
||||
raise ValueError('unknown SEA type %r' % typ)
|
||||
|
||||
|
||||
class SeaModule(Module):
|
||||
io = Attached()
|
||||
|
||||
path2param = None
|
||||
sea_object = None
|
||||
hdbpath = None # hdbpath for main writable
|
||||
|
||||
def __new__(cls, name, logger, cfgdict, srv):
|
||||
if hasattr(srv, 'extra_sea_modules'):
|
||||
extra_modules = srv.extra_sea_modules
|
||||
else:
|
||||
extra_modules = {}
|
||||
srv.extra_sea_modules = extra_modules
|
||||
json_file = cfgdict.pop('json_file', None) or SeaClient.default_json_file[cfgdict['io']]
|
||||
visibility_level = cfgdict.pop('visibility_level', 2)
|
||||
|
||||
single_module = cfgdict.pop('single_module', None)
|
||||
if single_module:
|
||||
sea_object, base, paramdesc = extra_modules[single_module]
|
||||
params = [paramdesc]
|
||||
paramdesc['key'] = 'value'
|
||||
if issubclass(cls, SeaWritable):
|
||||
if paramdesc.get('readonly', True):
|
||||
raise ConfigError('%s/%s is not writable' % (sea_object, paramdesc['path']))
|
||||
params.insert(0, paramdesc.copy()) # copy value
|
||||
paramdesc['key'] = 'target'
|
||||
paramdesc['readonly'] = False
|
||||
extra_module_set = ()
|
||||
if 'description' not in cfgdict:
|
||||
cfgdict['description'] = '%s@%s' % (single_module, json_file)
|
||||
else:
|
||||
sea_object = cfgdict.pop('sea_object')
|
||||
rel_paths = cfgdict.pop('rel_paths', '.')
|
||||
if 'description' not in cfgdict:
|
||||
cfgdict['description'] = '%s@%s%s' % (
|
||||
name, json_file, '' if rel_paths == '.' else ' (rel_paths=%s)' % rel_paths)
|
||||
|
||||
# with open(join(seaconfdir, json_file + '.json')) as fp:
|
||||
# sea_object, descr = json.load(fp)
|
||||
with open(join(seaconfdir, json_file)) as fp:
|
||||
content = json.load(fp)
|
||||
# print(json_file, content.keys())
|
||||
descr = content[sea_object]
|
||||
if rel_paths == '*' or not rel_paths:
|
||||
# take all
|
||||
main = descr['params'][0]
|
||||
if issubclass(cls, Readable):
|
||||
# assert main['path'] == '' # TODO: check cases where this fails
|
||||
main['key'] = 'value'
|
||||
else:
|
||||
descr['params'].pop(0)
|
||||
else:
|
||||
# filter by relative paths
|
||||
rel_paths = rel_paths.split()
|
||||
result = []
|
||||
is_running = None
|
||||
for rpath in rel_paths:
|
||||
include = True
|
||||
for paramdesc in descr['params']:
|
||||
path = paramdesc['path']
|
||||
if path.endswith('is_running') and issubclass(cls, Drivable):
|
||||
# take this independent of visibility
|
||||
is_running = paramdesc
|
||||
continue
|
||||
if paramdesc.get('visibility', 1) > visibility_level:
|
||||
continue
|
||||
sub = path.split('/', 1)
|
||||
if rpath == '.': # take all except subpaths with readonly node at top
|
||||
if len(sub) == 1:
|
||||
include = paramdesc.get('kids', 0) == 0 or not paramdesc.get('readonly', True)
|
||||
if include or path == '':
|
||||
result.append(paramdesc)
|
||||
elif sub[0] == rpath:
|
||||
result.append(paramdesc)
|
||||
if is_running: # take this at end
|
||||
result.append(is_running)
|
||||
descr['params'] = result
|
||||
rel0 = '' if rel_paths[0] == '.' else rel_paths[0]
|
||||
if result[0]['path'] == rel0:
|
||||
if issubclass(cls, Readable):
|
||||
result[0]['key'] = 'value'
|
||||
else:
|
||||
result.pop(0)
|
||||
else:
|
||||
logger.error('%s: no value found', name)
|
||||
# logger.info('PARAMS %s %r', name, result)
|
||||
base = descr['base']
|
||||
params = descr['params']
|
||||
if issubclass(cls, SeaWritable):
|
||||
paramdesc = params[0]
|
||||
assert paramdesc['key'] == 'value'
|
||||
params.append(paramdesc.copy()) # copy value?
|
||||
if paramdesc.get('readonly', True):
|
||||
raise ConfigError('%s/%s is not writable' % (sea_object, paramdesc['path']))
|
||||
paramdesc['key'] = 'target'
|
||||
paramdesc['readonly'] = False
|
||||
extra_module_set = cfgdict.pop('extra_modules', ())
|
||||
if extra_module_set:
|
||||
extra_module_set = set(extra_module_set.replace(',', ' ').split())
|
||||
path2param = {}
|
||||
attributes = dict(sea_object=sea_object, path2param=path2param)
|
||||
|
||||
# some guesses about visibility (may be overriden in *_cfg.py):
|
||||
if sea_object in ('table', 'cc'):
|
||||
attributes['visibility'] = 2
|
||||
elif base.count('/') > 1:
|
||||
attributes['visibility'] = 2
|
||||
for paramdesc in params:
|
||||
path = paramdesc['path']
|
||||
readonly = paramdesc.get('readonly', True)
|
||||
dt = get_datatype(paramdesc)
|
||||
#print('----', sea_object)
|
||||
#print(dt, paramdesc)
|
||||
kwds = dict(description=paramdesc.get('description', path),
|
||||
datatype=dt,
|
||||
visibility=paramdesc.get('visibility', 1),
|
||||
needscfg=False, readonly=readonly)
|
||||
if kwds['datatype'] is None:
|
||||
kwds.update(visibility=3, default='', datatype=StringType())
|
||||
pathlist = path.split('/') if path else []
|
||||
key = paramdesc.get('key') # None, 'value' or 'target'
|
||||
if key is None:
|
||||
if len(pathlist) > 0:
|
||||
if len(pathlist) == 1:
|
||||
if issubclass(cls, Readable):
|
||||
kwds['group'] = 'more'
|
||||
else:
|
||||
kwds['group'] = pathlist[-2]
|
||||
# flatten path to parameter name
|
||||
for i in reversed(range(len(pathlist))):
|
||||
key = '_'.join(pathlist[i:])
|
||||
if not key in cls.accessibles:
|
||||
break
|
||||
if key == 'is_running':
|
||||
kwds['export'] = False
|
||||
if key == 'target' and kwds.get('group') == 'more':
|
||||
kwds.pop('group')
|
||||
if key in cls.accessibles:
|
||||
if key == 'target':
|
||||
kwds['readonly'] = False
|
||||
prev = cls.accessibles[key]
|
||||
if key == 'status':
|
||||
# special case: status from sea is a string, not the status tuple
|
||||
pobj = prev.copy()
|
||||
else:
|
||||
pobj = Parameter(**kwds)
|
||||
merged_properties = prev.propertyValues.copy()
|
||||
pobj.updateProperties(merged_properties)
|
||||
pobj.merge(merged_properties)
|
||||
else:
|
||||
pobj = Parameter(**kwds)
|
||||
datatype = pobj.datatype
|
||||
if issubclass(cls, SeaWritable) and key == 'target':
|
||||
kwds['readonly'] = False
|
||||
attributes['value'] = Parameter(**kwds)
|
||||
|
||||
hdbpath = '/'.join([base] + pathlist)
|
||||
if key in extra_module_set:
|
||||
extra_modules[name + '.' + key] = sea_object, base, paramdesc
|
||||
continue # skip this parameter
|
||||
path2param.setdefault(hdbpath, []).append((name, key))
|
||||
attributes[key] = pobj
|
||||
# if hasattr(cls, 'read_' + key):
|
||||
# print('override %s.read_%s' % (cls.__name__, key))
|
||||
|
||||
def rfunc(self, cmd='hval %s/%s' % (base, path)):
|
||||
reply = self.io.query(cmd, True)
|
||||
try:
|
||||
reply = float(reply)
|
||||
except ValueError:
|
||||
pass
|
||||
# an updateEvent will be handled before above returns
|
||||
return reply
|
||||
|
||||
rfunc.poll = False
|
||||
attributes['read_' + key] = rfunc
|
||||
|
||||
if not readonly:
|
||||
# if hasattr(cls, 'write_' + key):
|
||||
# print('override %s.write_%s' % (cls.__name__, key))
|
||||
|
||||
def wfunc(self, value, datatype=datatype, command=paramdesc['cmd']):
|
||||
value = datatype.export_value(value)
|
||||
if isinstance(value, bool):
|
||||
value = int(value)
|
||||
# TODO: check if more has to be done for valid tcl data (strings?)
|
||||
cmd = "%s %s" % (command, value)
|
||||
self.io.query(cmd)
|
||||
return Done
|
||||
|
||||
attributes['write_' + key] = wfunc
|
||||
|
||||
# create standard parameters like value and status, if not yet there
|
||||
for pname, pobj in cls.accessibles.items():
|
||||
if pname == 'pollinterval':
|
||||
pobj.export = False
|
||||
attributes[pname] = pobj
|
||||
pobj.__set_name__(cls, pname)
|
||||
elif pname not in attributes and isinstance(pobj, Parameter):
|
||||
pobj.needscfg = False
|
||||
attributes[pname] = pobj
|
||||
pobj.__set_name__(cls, pname)
|
||||
|
||||
classname = '%s_%s' % (cls.__name__, name)
|
||||
newcls = type(classname, (cls,), attributes)
|
||||
result = Module.__new__(newcls)
|
||||
return result
|
||||
|
||||
def updateEvent(self, module, parameter, value, timestamp, readerror):
|
||||
upd = getattr(self, 'update_' + parameter, None)
|
||||
if upd:
|
||||
upd(value, timestamp, readerror)
|
||||
return
|
||||
try:
|
||||
pobj = self.parameters[parameter]
|
||||
except KeyError:
|
||||
print(self.name, parameter)
|
||||
raise
|
||||
pobj.timestamp = timestamp
|
||||
#if not pobj.readonly and pobj.value != value:
|
||||
# print('UPDATE', module, parameter, value)
|
||||
# should be done here: deal with clock differences
|
||||
if not readerror:
|
||||
try:
|
||||
pobj.value = value # store the value even in case of a validation error
|
||||
pobj.value = pobj.datatype(value)
|
||||
except Exception as e:
|
||||
readerror = secop_error(e)
|
||||
pobj.readerror = readerror
|
||||
if pobj.export:
|
||||
self.DISPATCHER.broadcast_event(make_update(self.name, pobj))
|
||||
|
||||
def initModule(self):
|
||||
self.io.register_obj(self, self.sea_object)
|
||||
super().initModule()
|
||||
|
||||
def doPoll(self):
|
||||
pass
|
||||
|
||||
|
||||
class SeaReadable(SeaModule, Readable):
|
||||
|
||||
def update_status(self, value, timestamp, readerror):
|
||||
if readerror:
|
||||
value = repr(readerror)
|
||||
if value == '':
|
||||
self.status = (self.Status.IDLE, '')
|
||||
else:
|
||||
self.status = (self.Status.ERROR, value)
|
||||
|
||||
def read_status(self):
|
||||
return self.status
|
||||
|
||||
|
||||
class SeaWritable(SeaModule, Writable):
|
||||
def read_value(self):
|
||||
return self.target
|
||||
|
||||
def update_target(self, value, timestamp, readerror):
|
||||
self.target = value
|
||||
if not readerror:
|
||||
self.value = value
|
||||
|
||||
|
||||
class SeaDrivable(SeaModule, Drivable):
|
||||
_sea_status = ''
|
||||
_is_running = 0
|
||||
|
||||
def read_status(self):
|
||||
return self.status
|
||||
|
||||
# def read_target(self):
|
||||
# return self.target
|
||||
|
||||
def write_target(self, value):
|
||||
self.io.query('run %s %s' % (self.sea_object, value))
|
||||
# self.status = [self.Status.BUSY, 'driving']
|
||||
return value
|
||||
|
||||
def update_status(self, value, timestamp, readerror):
|
||||
if not readerror:
|
||||
self._sea_status = value
|
||||
self.updateStatus()
|
||||
|
||||
def update_is_running(self, value, timestamp, readerror):
|
||||
if not readerror:
|
||||
self._is_running = value
|
||||
self.updateStatus()
|
||||
|
||||
def updateStatus(self):
|
||||
if self._sea_status:
|
||||
self.status = (self.Status.ERROR, self._sea_status)
|
||||
elif self._is_running:
|
||||
self.status = (self.Status.BUSY, 'driving')
|
||||
else:
|
||||
self.status = (self.Status.IDLE, '')
|
||||
|
||||
def updateTarget(self, module, parameter, value, timestamp, readerror):
|
||||
if value is not None:
|
||||
self.target = value
|
||||
|
||||
@Command()
|
||||
def stop(self):
|
||||
"""propagate to SEA
|
||||
|
||||
- on stdsct drivables this will call the halt script
|
||||
- on EaseDriv this will set the stopped state
|
||||
"""
|
||||
self.io.query('%s is_running 0' % self.sea_object)
|
307
frappy_psi/senis.py
Normal file
307
frappy_psi/senis.py
Normal file
@ -0,0 +1,307 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# *****************************************************************************
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Module authors:
|
||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||
# *****************************************************************************
|
||||
"""senis hall sensor"""
|
||||
|
||||
|
||||
import threading
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
from serial import Serial
|
||||
|
||||
from frappy.core import Attached, BoolType, FloatRange, IntRange, \
|
||||
Parameter, Property, Readable, StringType, TupleOf
|
||||
|
||||
|
||||
class Temperature(Readable):
|
||||
enablePoll = False
|
||||
|
||||
value = Parameter(datatype=FloatRange(unit='degC'))
|
||||
|
||||
|
||||
class Bcomp(Readable):
|
||||
enablePoll = False
|
||||
|
||||
value = Parameter(datatype=FloatRange(unit='T'))
|
||||
range = Parameter('working range', FloatRange(unit='T'), default=0)
|
||||
|
||||
|
||||
class Raw(Readable):
|
||||
enablePoll = False
|
||||
|
||||
value = Parameter(datatype=FloatRange())
|
||||
|
||||
|
||||
class TeslameterBase(Readable):
|
||||
"""code for both models
|
||||
|
||||
the protocol is somewhat weird as the read command 'B' initiates a permanent update
|
||||
which has to be stopped in between the value polls and for other communication.
|
||||
|
||||
the B components (and temperatures for 3MH6) are implemented as separate modules
|
||||
"""
|
||||
x = Attached()
|
||||
y = Attached()
|
||||
z = Attached()
|
||||
|
||||
value = Parameter('B vector',
|
||||
datatype=TupleOf(FloatRange(unit='T'), FloatRange(unit='T'), FloatRange(unit='T')))
|
||||
usb = Parameter('usb device', StringType(), readonly=False)
|
||||
enabled = Parameter('enable data acq', datatype=BoolType(), readonly=False, default=True)
|
||||
nsample = Parameter('number of samples for average', datatype=IntRange(1, 1000), readonly=False, default=1)
|
||||
|
||||
def init_serial(self, baud):
|
||||
self._conn = Serial(self.usb, baud, timeout=0.1)
|
||||
self._lock = threading.Lock()
|
||||
self.stop_reading()
|
||||
|
||||
def write_bytes(self, msg):
|
||||
with self._lock:
|
||||
self._conn.write(msg)
|
||||
|
||||
def read_bytes(self, cnt):
|
||||
with self._lock:
|
||||
return self._conn.read(cnt)
|
||||
|
||||
def stop_reading(self):
|
||||
self.write_bytes(b'S')
|
||||
self.read_bytes(9999) # swallow bytes until timeout
|
||||
|
||||
def write_enabled(self, value):
|
||||
if value:
|
||||
self.status = self.Status.IDLE, ''
|
||||
else:
|
||||
self.status = self.Status.DISABLED, 'disabled'
|
||||
self._x.status = self._y.status = self._z.status = self.status
|
||||
return value
|
||||
|
||||
|
||||
class Teslameter3MH3(TeslameterBase):
|
||||
"""simpler model without temperature and auto range
|
||||
|
||||
remark: no query for the sample rate is possible, therefore set always to
|
||||
a default rate (therefore initwrite=True on the rate parameter)
|
||||
"""
|
||||
range = Property('full scale', datatype=FloatRange(), default=2)
|
||||
|
||||
def earlyInit(self):
|
||||
self.init_serial(115200)
|
||||
self.write_bytes(b'C') # put into calibrated mode
|
||||
if self.read_bytes(1) != b'!':
|
||||
self.log.error('missing response to C command')
|
||||
self.write_bytes(b'A\x80\r') # set to 90 SPS
|
||||
self.read_bytes(1) # wait 0.1 sec as we get no reply
|
||||
|
||||
def read_value(self):
|
||||
if not self.enabled:
|
||||
return self.value
|
||||
s = self._conn
|
||||
s.timeout = 0.1 + 0.02 * self.nsample
|
||||
for _ in range(2):
|
||||
self.write_bytes(b'B')
|
||||
# t = time.time()
|
||||
reply = self.read_bytes(8 * self.nsample)
|
||||
s.timeout = 0.1
|
||||
self.stop_reading()
|
||||
remainder = len(reply) % 8
|
||||
if remainder:
|
||||
reply = reply[:-remainder]
|
||||
if not reply:
|
||||
continue
|
||||
data = np.frombuffer(reply, dtype='i1,3<i2,i1')
|
||||
# first byte must be 'B' and last byte must be CR
|
||||
if np.all(data['f0'] == ord(b'B')) and np.all(data['f2'] == 13):
|
||||
break
|
||||
else:
|
||||
self.status = self.Status.ERROR, 'bad reply'
|
||||
raise ValueError('bad reply')
|
||||
self.status = self.Status.IDLE, ''
|
||||
value = np.average(data['f1'], axis=0) * self.range / 20000.
|
||||
self._x.value, self._y.value, self._z.value = value
|
||||
self._x.range = self._y.range =self._z.range = self.range
|
||||
return value
|
||||
|
||||
|
||||
class Teslameter3MH6(TeslameterBase):
|
||||
"""luxury model with probe and box temperature and autorange"""
|
||||
x_direct = Attached()
|
||||
y_direct = Attached()
|
||||
z_direct = Attached()
|
||||
probe_temp = Attached()
|
||||
box_temp = Attached()
|
||||
probe_temp_direct = Attached()
|
||||
box_temp_direct = Attached()
|
||||
|
||||
range = Parameter('range or 0 for autorange', FloatRange(0, 20, unit='T'), readonly=False, default=0)
|
||||
rate = Parameter('sampling rate', datatype=FloatRange(10, 15000, unit='Hz'),
|
||||
readonly=False)
|
||||
avtime = Parameter('data acquisition time', FloatRange(), default=0)
|
||||
|
||||
SAMPLING_RATES = {0xe0: 15000, 0xd0: 7500, 0xc0: 3750, 0xb0: 2000, 0xa1: 1000,
|
||||
0x92: 500, 0x82: 100, 0x72: 60, 0x63: 50, 0x53: 30, 0x23: 10}
|
||||
RANGES = dict(zip(b'1234', [0.1, 0.5, 2, 20]))
|
||||
|
||||
def earlyInit(self):
|
||||
self.init_serial(3000000)
|
||||
self.write_rate(10)
|
||||
|
||||
def get_data(self):
|
||||
for _ in range(2):
|
||||
self.write_bytes(b'B')
|
||||
reply = self.read_bytes(25 * self.nsample)
|
||||
self.stop_reading()
|
||||
remainder = len(reply) % 25
|
||||
if remainder:
|
||||
reply = reply[:-remainder]
|
||||
if not reply:
|
||||
continue
|
||||
chk = np.frombuffer(reply, dtype='i1,23i1,i1')
|
||||
if not np.all(np.sum(chk['f1'], axis=1) % 256 == 0):
|
||||
status = 'checksum error'
|
||||
continue
|
||||
# first byte must be 'B' and last byte must be CR
|
||||
if np.all(chk['f0'] == ord(b'B')) and np.all(chk['f2'] == 13):
|
||||
break
|
||||
status = 'bad reply'
|
||||
else:
|
||||
self.status = self.Status.ERROR, status
|
||||
raise ValueError(status)
|
||||
self.status = self.Status.IDLE, ''
|
||||
return reply
|
||||
|
||||
def read_value(self):
|
||||
if not self.enabled:
|
||||
return self.value
|
||||
t0 = time.time()
|
||||
s = self._conn
|
||||
s.timeout = 0.1 + self.nsample / self.rate
|
||||
self.write_bytes(b'C') # put into calibrated mode
|
||||
if self.read_bytes(1) != b'c':
|
||||
self.log.error('missing response to C command')
|
||||
reply = self.get_data()
|
||||
data = np.frombuffer(reply,
|
||||
dtype=[('_head', 'i1'),
|
||||
('x', '>f4'), ('thc', '>f4'), ('y', '>f4'), ('z', '>f4'),
|
||||
('_ted', '>i2'), ('tec', '>f4'), ('_tail', 'i2')])
|
||||
|
||||
mean = {}
|
||||
for key in data.dtype.fields:
|
||||
if not key.startswith('_'):
|
||||
mean[key] = np.average(data[key])
|
||||
|
||||
self._x.value = mean['x'] * 0.001
|
||||
self._y.value = mean['y'] * 0.001
|
||||
self._z.value = mean['z'] * 0.001
|
||||
self._probe_temp.value = mean['thc']
|
||||
self._box_temp.value = mean['tec']
|
||||
|
||||
self.write_bytes(b'D') # put into NONcalibrated mode
|
||||
if self.read_bytes(1) != b'd':
|
||||
self.log.error('missing response to D command')
|
||||
reply = self.get_data()
|
||||
data = np.frombuffer(reply,
|
||||
dtype=[('_head', 'i1'),
|
||||
('x', '>i4'), ('thc', '>i4'), ('y', '>i4'), ('z', '>i4'),
|
||||
('_ted', '>i2'), ('tec', '>i4'), ('_tail', 'i2')])
|
||||
for key in data.dtype.fields:
|
||||
if not key.startswith('_'):
|
||||
mean[key] = np.average(data[key])
|
||||
|
||||
self._x_direct.value = mean['x']
|
||||
self._y_direct.value = mean['y']
|
||||
self._z_direct.value = mean['z']
|
||||
self._probe_temp_direct.value = mean['thc']
|
||||
self._box_temp_direct.value = mean['tec'] * 0.01
|
||||
|
||||
self.avtime = time.time() - t0
|
||||
return self._x.value, self._y.value, self._z.value
|
||||
|
||||
def get_rate_code(self, value):
|
||||
for rate_code, sr in sorted(self.SAMPLING_RATES.items(), key=lambda kv: kv[1]):
|
||||
if value < sr * 1.1:
|
||||
break
|
||||
return sr, rate_code
|
||||
|
||||
def write_rate(self, value):
|
||||
sr, code = self.get_rate_code(value)
|
||||
for _ in range(2):
|
||||
self.write_bytes(b'K%2.2x' % code)
|
||||
if self.read_bytes(2) == b'k%c' % code:
|
||||
break
|
||||
self.stop_reading()
|
||||
else:
|
||||
raise ValueError('bad response from rate command')
|
||||
return sr
|
||||
|
||||
def read_rate(self):
|
||||
self.write_bytes(b'K?')
|
||||
reply = self.read_bytes(2)
|
||||
if reply[0:1] != b'k':
|
||||
raise ValueError('bad response from rate query')
|
||||
return self.SAMPLING_RATES[reply[1]]
|
||||
|
||||
def read_range(self):
|
||||
self.write_bytes(b'amr?')
|
||||
reply = self.read_bytes(5)
|
||||
if reply == b'arng:':
|
||||
ranges = [self.RANGES[c] for c in self.read_bytes(3)]
|
||||
result = 0
|
||||
elif reply == b'mrng:':
|
||||
ranges = [self.RANGES[self.read_bytes(1)[0]]] * 3
|
||||
result = ranges[0]
|
||||
else:
|
||||
raise ValueError('bad reply to range query %s' % repr(reply))
|
||||
self._x.range, self._y.range, self._z.range = ranges
|
||||
return result
|
||||
|
||||
def write_range(self, value):
|
||||
status = None
|
||||
for _ in range(2):
|
||||
if status:
|
||||
self.stop_reading()
|
||||
try:
|
||||
rng = self.read_range()
|
||||
except ValueError:
|
||||
status = 'can not read range'
|
||||
continue
|
||||
if value == rng:
|
||||
return value
|
||||
if value == 0:
|
||||
self.write_bytes(b'T')
|
||||
if self.read_bytes(3) != b'T-1':
|
||||
status = 'bad reply to auto range command'
|
||||
continue
|
||||
return 0
|
||||
if rng == 0:
|
||||
self.write_bytes(b'T')
|
||||
if self.read_bytes(3) != b'T-0':
|
||||
status = 'bad reply to toggle manual range command'
|
||||
continue
|
||||
for code, rng in sorted(self.RANGES.items()):
|
||||
if value < rng * 1.1:
|
||||
break
|
||||
self.write_bytes(b'mr%c' % code)
|
||||
reply = self.read_bytes(6)
|
||||
if reply != b'mrng:%c' % code:
|
||||
status = 'bad reply to manual range command %s' % repr(reply)
|
||||
continue
|
||||
return rng
|
||||
raise ValueError(status)
|
49
frappy_psi/simdpm.py
Normal file
49
frappy_psi/simdpm.py
Normal file
@ -0,0 +1,49 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# *****************************************************************************
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Module authors:
|
||||
# M. Zolliker <markus.zolliker@psi.ch>
|
||||
#
|
||||
# *****************************************************************************
|
||||
"""simulated transducer DPM3 read out"""
|
||||
|
||||
import random
|
||||
import math
|
||||
from frappy.core import Readable, Parameter, FloatRange, Attached
|
||||
from frappy.lib import clamp
|
||||
|
||||
|
||||
class DPM3(Readable):
|
||||
motor = Attached()
|
||||
jitter = Parameter('simulated jitter', FloatRange(unit='N'), default=0.1, readonly=False)
|
||||
hysteresis = Parameter('simulated hysteresis', FloatRange(unit='deg'), default=100, readonly=False)
|
||||
friction = Parameter('friction', FloatRange(unit='N/deg'), default=2.5, readonly=False)
|
||||
slope = Parameter('slope', FloatRange(unit='N/deg'), default=0.5, readonly=False)
|
||||
offset = Parameter('offset', FloatRange(unit='N'), default=0, readonly=False)
|
||||
|
||||
_pos = 0
|
||||
|
||||
def read_value(self):
|
||||
mot = self._motor
|
||||
d = self.friction * self.slope
|
||||
self._pos = clamp(self._pos, mot.value - d, mot.value + d)
|
||||
f = (mot.value - self._pos) / self.slope
|
||||
if mot.value > 0:
|
||||
f = max(f, (mot.value - self.hysteresis) / self.slope)
|
||||
else:
|
||||
f = min(f, (mot.value + self.hysteresis) / self.slope)
|
||||
return f + self.jitter * (random.random() - random.random()) * 0.5
|
320
frappy_psi/triton.py
Normal file
320
frappy_psi/triton.py
Normal file
@ -0,0 +1,320 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# *****************************************************************************
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Module authors:
|
||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||
# *****************************************************************************
|
||||
"""oxford instruments triton (kelvinoxjt dil)"""
|
||||
|
||||
from math import sqrt, log10
|
||||
from frappy.core import Writable, Parameter, Readable, Drivable, IDLE, WARN, BUSY, ERROR, \
|
||||
Done, Property
|
||||
from frappy.datatypes import EnumType, FloatRange, StringType
|
||||
from frappy.lib.enum import Enum
|
||||
from frappy_psi.mercury import MercuryChannel, Mapped, off_on, HasInput, SELF
|
||||
from frappy.lib import clamp
|
||||
import frappy_psi.mercury as mercury
|
||||
|
||||
actions = Enum(none=0, condense=1, circulate=2, collect=3)
|
||||
open_close = Mapped(CLOSE=0, OPEN=1)
|
||||
actions_map = Mapped(STOP=actions.none, COND=actions.condense, COLL=actions.collect)
|
||||
actions_map.mapping['NONE'] = actions.none # when writing, STOP is used instead of NONE
|
||||
|
||||
|
||||
class Action(MercuryChannel, Writable):
|
||||
channel_type = 'ACTN'
|
||||
cooldown_channel = Property('cool down channel', StringType(), 'T5')
|
||||
mix_channel = Property('mix channel', StringType(), 'T5')
|
||||
value = Parameter('running action', EnumType(actions))
|
||||
target = Parameter('action to do', EnumType(none=0, condense=1, collect=3), readonly=False)
|
||||
_target = 0
|
||||
|
||||
def read_value(self):
|
||||
return self.query('SYS:DR:ACTN', actions_map)
|
||||
|
||||
def read_target(self):
|
||||
return self._target
|
||||
|
||||
def write_target(self, value):
|
||||
self._target = value
|
||||
self.change('SYS:DR:CHAN:COOL', self.cooldown_channel, str)
|
||||
# self.change('SYS:DR:CHAN:MC', self.mix_channel, str)
|
||||
# self.change('DEV:T5:TEMP:MEAS:ENAB', 'ON', str)
|
||||
return self.change('SYS:DR:ACTN', value, actions_map)
|
||||
|
||||
# actions:
|
||||
# NONE (no action)
|
||||
# COND (condense mixture)
|
||||
# COLL (collect mixture)
|
||||
# STOP (go to NONE)
|
||||
#
|
||||
# not yet used (would need a subclass of Action):
|
||||
# CLDN (cool down)
|
||||
# PCL (precool automation)
|
||||
# PCOND (pause pre-cool (not condense?) automation)
|
||||
# RCOND (resume pre-cool (not condense?) automation)
|
||||
# WARM (warm-up)
|
||||
# EPCL (empty pre-cool automation)
|
||||
|
||||
|
||||
class Valve(MercuryChannel, Drivable):
|
||||
channel_type = 'VALV'
|
||||
value = Parameter('valve state', EnumType(closed=0, opened=1))
|
||||
target = Parameter('valve target', EnumType(close=0, open=1))
|
||||
|
||||
_try_count = None
|
||||
|
||||
def doPoll(self):
|
||||
self.read_status()
|
||||
|
||||
def read_value(self):
|
||||
return self.query('VALV:SIG:STATE', open_close)
|
||||
|
||||
def read_status(self):
|
||||
pos = self.read_value()
|
||||
if self._try_count is None: # not switching
|
||||
return IDLE, ''
|
||||
if pos == self.target:
|
||||
# success
|
||||
if self._try_count:
|
||||
# make sure last sent command was not opposite
|
||||
self.change('VALV:SIG:STATE', self.target, open_close)
|
||||
self._try_count = None
|
||||
self.setFastPoll(False)
|
||||
return IDLE, ''
|
||||
self._try_count += 1
|
||||
if self._try_count % 4 == 0:
|
||||
# send to opposite position in order to unblock
|
||||
self.change('VALV:SIG:STATE', pos, open_close)
|
||||
return BUSY, 'unblock'
|
||||
if self._try_count > 9:
|
||||
# make sure system does not toggle later
|
||||
self.change('VALV:SIG:STATE', pos, open_close)
|
||||
return ERROR, 'can not %s valve' % self.target.name
|
||||
self.change('VALV:SIG:STATE', self.target, open_close)
|
||||
return BUSY, 'waiting'
|
||||
|
||||
def write_target(self, value):
|
||||
if value != self.read_value():
|
||||
self._try_count = 0
|
||||
self.setFastPoll(True, 0.25)
|
||||
self.change('VALV:SIG:STATE', value, open_close)
|
||||
self.status = BUSY, self.target.name
|
||||
return value
|
||||
|
||||
|
||||
class Pump(MercuryChannel, Writable):
|
||||
channel_type = 'PUMP'
|
||||
value = Parameter('pump state', EnumType(off=0, on=1))
|
||||
target = Parameter('pump target', EnumType(off=0, on=1))
|
||||
|
||||
def read_value(self):
|
||||
return self.query('PUMP:SIG:STATE', off_on)
|
||||
|
||||
def write_target(self, value):
|
||||
return self.change('PUMP:SIG:STATE', value, off_on)
|
||||
|
||||
def read_status(self):
|
||||
return IDLE, ''
|
||||
|
||||
|
||||
class TurboPump(Pump):
|
||||
power = Parameter('pump power', FloatRange(unit='W'))
|
||||
freq = Parameter('pump frequency', FloatRange(unit='Hz'))
|
||||
powerstage_temp = Parameter('temperature of power stage', FloatRange(unit='K'))
|
||||
motor_temp = Parameter('temperature of motor', FloatRange(unit='K'))
|
||||
bearing_temp = Parameter('temperature of bearing', FloatRange(unit='K'))
|
||||
pumpbase_temp = Parameter('temperature of pump base', FloatRange(unit='K'))
|
||||
electronics_temp = Parameter('temperature of electronics', FloatRange(unit='K'))
|
||||
|
||||
def read_status(self):
|
||||
status = self.query('PUMP:STATUS', str)
|
||||
if status == 'OK':
|
||||
return IDLE, ''
|
||||
return WARN, status
|
||||
|
||||
def read_power(self):
|
||||
return self.query('PUMP:SIG:POWR')
|
||||
|
||||
def read_freq(self):
|
||||
return self.query('PUMP:SIG:SPD')
|
||||
|
||||
def read_powerstage_temp(self):
|
||||
return self.query('PUMP:SIG:PST')
|
||||
|
||||
def read_motor_temp(self):
|
||||
return self.query('PUMP:SIG:MT')
|
||||
|
||||
def read_bearing_temp(self):
|
||||
return self.query('PUMP:SIG:BT')
|
||||
|
||||
def read_pumpbase_temp(self):
|
||||
return self.query('PUMP:SIG:PBT')
|
||||
|
||||
def read_electronics_temp(self):
|
||||
return self.query('PUMP:SIG:ET')
|
||||
|
||||
|
||||
# class PulseTubeCompressor(MercuryChannel, Writable):
|
||||
# channel_type = 'PTC'
|
||||
# value = Parameter('compressor state', EnumType(closed=0, opened=1))
|
||||
# target = Parameter('compressor target', EnumType(close=0, open=1))
|
||||
# water_in_temp = Parameter('temperature of water inlet', FloatRange(unit='K'))
|
||||
# water_out_temp = Parameter('temperature of water outlet', FloatRange(unit='K'))
|
||||
# helium_temp = Parameter('temperature of helium', FloatRange(unit='K'))
|
||||
# helium_low_pressure = Parameter('helium pressure (low side)', FloatRange(unit='mbar'))
|
||||
# helium_high_pressure = Parameter('helium pressure (high side)', FloatRange(unit='mbar'))
|
||||
# motor_current = Parameter('motor current', FloatRange(unit='A'))
|
||||
#
|
||||
# def read_value(self):
|
||||
# return self.query('PTC:SIG:STATE', off_on)
|
||||
#
|
||||
# def write_target(self, value):
|
||||
# return self.change('PTC:SIG:STATE', value, off_on)
|
||||
#
|
||||
# def read_status(self):
|
||||
# # TODO: check possible status values
|
||||
# return self.WARN, self.query('PTC:SIG:STATUS')
|
||||
#
|
||||
# def read_water_in_temp(self):
|
||||
# return self.query('PTC:SIG:WIT')
|
||||
#
|
||||
# def read_water_out_temp(self):
|
||||
# return self.query('PTC:SIG:WOT')
|
||||
#
|
||||
# def read_helium_temp(self):
|
||||
# return self.query('PTC:SIG:HT')
|
||||
#
|
||||
# def read_helium_low_pressure(self):
|
||||
# return self.query('PTC:SIG:HLP')
|
||||
#
|
||||
# def read_helium_high_pressure(self):
|
||||
# return self.query('PTC:SIG:HHP')
|
||||
#
|
||||
# def read_motor_current(self):
|
||||
# return self.query('PTC:SIG:MCUR')
|
||||
|
||||
|
||||
class FlowMeter(MercuryChannel, Readable):
|
||||
channel_type = 'FLOW'
|
||||
|
||||
def read_value(self):
|
||||
return self.query('FLOW:SIG:FLOW')
|
||||
|
||||
|
||||
class ScannerChannel:
|
||||
# TODO: excitation, enable
|
||||
# TODO: switch on/off filter, check
|
||||
filter_time = Parameter('filter time', FloatRange(1, 200, unit='sec'), readonly=False)
|
||||
dwell_time = Parameter('dwell time', FloatRange(1, 200, unit='sec'), readonly=False)
|
||||
pause_time = Parameter('pause time', FloatRange(3, 200, unit='sec'), readonly=False)
|
||||
|
||||
def read_filter_time(self):
|
||||
return self.query('TEMP:FILT:TIME')
|
||||
|
||||
def write_filter_time(self, value):
|
||||
self.change('TEMP:FILT:WIN', 80)
|
||||
return self.change('TEMP:FILT:TIME', value)
|
||||
|
||||
def read_dwell_time(self):
|
||||
return self.query('TEMP:MEAS:DWEL')
|
||||
|
||||
def write_dwell_time(self, value):
|
||||
self.change('TEMP:FILT:WIN', 80)
|
||||
return self.change('TEMP:MEAS:DWEL', value)
|
||||
|
||||
def read_pause_time(self):
|
||||
return self.query('TEMP:MEAS:PAUS')
|
||||
|
||||
def write_pause_time(self, value):
|
||||
return self.change('TEMP:MEAS:PAUS', value)
|
||||
|
||||
|
||||
class TemperatureSensor(ScannerChannel, mercury.TemperatureSensor):
|
||||
pass
|
||||
|
||||
|
||||
class TemperatureLoop(ScannerChannel, mercury.TemperatureLoop):
|
||||
ENABLE = 'TEMP:LOOP:MODE'
|
||||
ENABLE_RAMP = 'TEMP:LOOP:RAMP:ENAB'
|
||||
RAMP_RATE = 'TEMP:LOOP:RAMP:RATE'
|
||||
|
||||
enable_pid_table = None # remove, does not work on triton
|
||||
ctrlpars = Parameter('pid (gain, integral (inv. time), differential time')
|
||||
system_channel = Property('system channel name', StringType(), 'MC')
|
||||
|
||||
def write_control_active(self, value):
|
||||
if self.system_channel:
|
||||
self.change('SYS:DR:CHAN:%s' % self.system_channel, self.slot.split(',')[0], str)
|
||||
if value:
|
||||
self.change('TEMP:LOOP:FILT:ENAB', 'ON', str)
|
||||
if self.output_module:
|
||||
limit = self.output_module.read_limit() or None # None: max. limit
|
||||
self.output_module.write_limit(limit)
|
||||
return super().write_control_active(value)
|
||||
|
||||
|
||||
class HeaterOutput(HasInput, MercuryChannel, Writable):
|
||||
"""heater output"""
|
||||
channel_type = 'HTR'
|
||||
value = Parameter('heater output', FloatRange(unit='uW'))
|
||||
target = Parameter('heater output', FloatRange(0, unit='$'), readonly=False)
|
||||
resistivity = Parameter('heater resistivity', FloatRange(unit='Ohm'))
|
||||
|
||||
def read_resistivity(self):
|
||||
return self.query('HTR:RES')
|
||||
|
||||
def read_value(self):
|
||||
return round(self.query('HTR:SIG:POWR'), 3)
|
||||
|
||||
def read_target(self):
|
||||
if self.controlled_by != 0:
|
||||
return Done
|
||||
return self.value
|
||||
|
||||
def write_target(self, value):
|
||||
self.write_controlled_by(SELF)
|
||||
if self.resistivity:
|
||||
# round to the next voltage step
|
||||
value = round(sqrt(value * self.resistivity)) ** 2 / self.resistivity
|
||||
return round(self.change('HTR:SIG:POWR', value), 3)
|
||||
|
||||
|
||||
class HeaterOutputWithRange(HeaterOutput):
|
||||
"""heater output with heater range"""
|
||||
channel_type = 'HTR,TEMP'
|
||||
|
||||
limit = Parameter('max. heater power', FloatRange(unit='uW'), readonly=False)
|
||||
|
||||
def read_limit(self):
|
||||
maxcur = self.query('TEMP:LOOP:RANGE') # mA
|
||||
return self.read_resistivity() * maxcur ** 2 # uW
|
||||
|
||||
def write_limit(self, value):
|
||||
if value is None:
|
||||
maxcur = 100 # max. allowed current 100mA
|
||||
else:
|
||||
maxcur = sqrt(value / self.read_resistivity())
|
||||
for cur in 0.0316, 0.1, 0.316, 1, 3.16, 10, 31.6, 100:
|
||||
if cur > maxcur * 0.999:
|
||||
maxcur = cur
|
||||
break
|
||||
else:
|
||||
maxcur = cur
|
||||
self.change('TEMP:LOOP:RANGE', maxcur)
|
||||
return self.read_limit()
|
||||
|
261
frappy_psi/ultrasound.py
Normal file
261
frappy_psi/ultrasound.py
Normal file
@ -0,0 +1,261 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# *****************************************************************************
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Module authors:
|
||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||
# *****************************************************************************
|
||||
"""frappy support for ultrasound"""
|
||||
|
||||
import math
|
||||
#import serial
|
||||
import os
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
|
||||
import frappy_psi.iqplot as iqplot
|
||||
from frappy_psi.adq_mr import Adq
|
||||
from frappy.core import Attached, BoolType, Done, FloatRange, HasIO, \
|
||||
IntRange, Module, Parameter, Readable, StringIO, StringType
|
||||
from frappy.properties import Property
|
||||
|
||||
|
||||
def fname_from_time(t, extension):
|
||||
tm = time.localtime(t)
|
||||
dirname = os.path.join('..', 'data', time.strftime("%Y-%m-%d_%H", tm))
|
||||
filename = time.strftime("%Y-%m-%d_%H-%M-%S_", tm)
|
||||
filename = filename + ("%.1f" % t)[-1]
|
||||
if not os.path.isdir(dirname):
|
||||
os.makedirs(dirname)
|
||||
return os.path.join(dirname, filename)
|
||||
|
||||
|
||||
class Roi(Readable):
|
||||
main = Attached()
|
||||
|
||||
value = Parameter('amplitude', FloatRange(), default=0)
|
||||
phase = Parameter('phase', FloatRange(unit='deg'), default=0)
|
||||
i = Parameter('in phase', FloatRange(), default=0)
|
||||
q = Parameter('out of phase', FloatRange(), default=0)
|
||||
time = Parameter('start time', FloatRange(unit='nsec'), readonly=False)
|
||||
size = Parameter('interval (symmetric around time)', FloatRange(unit='nsec'), readonly=False)
|
||||
enable = Parameter('calculate this roi', BoolType(), readonly=False, default=True)
|
||||
#status = Parameter(export=False)
|
||||
pollinterval = Parameter(export=False)
|
||||
|
||||
interval = (0,0)
|
||||
|
||||
def initModule(self):
|
||||
super().initModule()
|
||||
self.main.register_roi(self)
|
||||
self.calc_interval()
|
||||
|
||||
def calc_interval(self):
|
||||
self.interval = (self.time - 0.5 * self.size, self.time + 0.5 * self.size)
|
||||
|
||||
def write_time(self, value):
|
||||
self.time = value
|
||||
self.calc_interval()
|
||||
return Done
|
||||
|
||||
def write_size(self, value):
|
||||
self.size = value
|
||||
self.calc_interval()
|
||||
return Done
|
||||
|
||||
|
||||
class Pars(Module):
|
||||
description = 'relevant parameters from SEA'
|
||||
|
||||
timestamp = Parameter('unix timestamp', StringType(), default='0', readonly=False)
|
||||
temperature = Parameter('T', FloatRange(unit='K'), default=0, readonly=False)
|
||||
mf = Parameter('field', FloatRange(unit='T'), default=0, readonly=False)
|
||||
sr = Parameter('rotaion angle', FloatRange(unit='deg'), default=0, readonly=False)
|
||||
|
||||
|
||||
class FreqStringIO(StringIO):
|
||||
end_of_line = '\r'
|
||||
|
||||
|
||||
class Frequency(HasIO, Readable):
|
||||
pars = Attached()
|
||||
sr = Property('samples per record', datatype=IntRange(), default=16384)
|
||||
maxy = Property('plot y scale', datatype=FloatRange(), default=0.5)
|
||||
|
||||
value = Parameter('frequency@I,q', datatype=FloatRange(unit='Hz'), default=0)
|
||||
basefreq = Parameter('base frequency', FloatRange(unit='Hz'), readonly=False)
|
||||
nr = Parameter('number of records', datatype=IntRange(1,10000), default=500)
|
||||
freq = Parameter('target frequency', FloatRange(unit='Hz'), readonly=False)
|
||||
bw = Parameter('bandwidth lowpassfilter', datatype=FloatRange(unit='Hz'),default=10E6)
|
||||
amp = Parameter('amplitude', FloatRange(unit='dBm'), readonly=False)
|
||||
control = Parameter('control loop on?', BoolType(), readonly=False, default=True)
|
||||
time = Parameter('pulse start time', FloatRange(unit='nsec'),
|
||||
readonly=False)
|
||||
size = Parameter('pulse length (starting from time)', FloatRange(unit='nsec'),
|
||||
readonly=False)
|
||||
pulselen = Parameter('adjusted pulse length (integer number of periods)', FloatRange(unit='nsec'), default=1)
|
||||
maxstep = Parameter('max frequency step', FloatRange(unit='Hz'), readonly=False,
|
||||
default=10000)
|
||||
minstep = Parameter('min frequency step for slope calculation', FloatRange(unit='Hz'),
|
||||
readonly=False, default=4000)
|
||||
slope = Parameter('inphase/frequency slope', FloatRange(), readonly=False,
|
||||
default=1e6)
|
||||
plot = Parameter('create plot images', BoolType(), readonly=False, default=True)
|
||||
save = Parameter('save data', BoolType(), readonly=False, default=True)
|
||||
pollinterval = Parameter(datatype=FloatRange(0,120))
|
||||
|
||||
ioClass = FreqStringIO
|
||||
|
||||
lastfreq = None
|
||||
old = None
|
||||
starttime = None
|
||||
interval = (0,0)
|
||||
|
||||
def earlyInit(self):
|
||||
super().earlyInit()
|
||||
self.adq = Adq(self.nr, self.sr, self.bw)
|
||||
self.roilist = []
|
||||
self.write_nr(self.nr)
|
||||
self.skipctrl = 0
|
||||
self.plotter = iqplot.Plot(self.maxy)
|
||||
self.calc_interval()
|
||||
|
||||
def calc_interval(self):
|
||||
self.interval = (self.time, self.time + self.size)
|
||||
|
||||
def write_time(self, value):
|
||||
self.time = value
|
||||
self.calc_interval()
|
||||
return Done
|
||||
|
||||
def write_size(self, value):
|
||||
self.size = value
|
||||
self.calc_interval()
|
||||
return Done
|
||||
|
||||
def write_nr(self, value):
|
||||
# self.pollinterval = value * 0.0001
|
||||
return value
|
||||
|
||||
def register_roi(self, roi):
|
||||
self.roilist.append(roi)
|
||||
|
||||
def set_freq(self):
|
||||
freq = self.freq + self.basefreq
|
||||
self.communicate('FREQ %.15g;FREQ?' % freq)
|
||||
#self._iodev.readline().decode('ascii')
|
||||
return freq
|
||||
|
||||
def write_amp(self, amp):
|
||||
reply = self.communicate('AMPR %g;AMPR?' % amp)
|
||||
return float(reply)
|
||||
|
||||
def read_amp(self):
|
||||
reply = self.communicate('AMPR?')
|
||||
return float(reply)
|
||||
|
||||
def write_freq(self, value):
|
||||
self.skipctrl = 2 # suppress control for the 2 next steps
|
||||
return value
|
||||
|
||||
def doPoll(self):
|
||||
"""main poll loop body"""
|
||||
if self.lastfreq is None:
|
||||
self.lastfreq = self.set_freq()
|
||||
self.adq.start()
|
||||
if self.starttime is None:
|
||||
self.starttime = time.time()
|
||||
times = []
|
||||
times.append(('init', time.time()))
|
||||
seadata = {p: float(getattr(self.pars, p)) for p in self.pars.parameters}
|
||||
data = self.adq.getdata() # this will wait, if not yet finished
|
||||
#save sample
|
||||
#np.save('sample.dat',data)
|
||||
times.append(('wait',time.time()))
|
||||
if self.control:
|
||||
freq = self.lastfreq # data was acquired at this freq
|
||||
else:
|
||||
freq = self.set_freq()
|
||||
seadata['frequency'] = freq
|
||||
if self.control:
|
||||
self.lastfreq = self.set_freq()
|
||||
times.append(('setf',time.time()))
|
||||
self.adq.start() # start next acq
|
||||
times.append(('start',time.time()))
|
||||
roilist = [r for r in self.roilist if r.enable]
|
||||
|
||||
gates = self.adq.gates_and_curves(data, freq, self.interval,
|
||||
[r.interval for r in roilist])
|
||||
if self.save:
|
||||
times.append(('save',time.time()))
|
||||
tdata, idata, qdata, pdata = self.adq.curves
|
||||
seadata['timestep'] = tdata[1] - tdata[0]
|
||||
iqdata = np.array((idata, qdata, pdata), dtype='f4')
|
||||
ts = seadata['timestamp']
|
||||
if ts:
|
||||
filename = fname_from_time(ts, '.npz')
|
||||
seanp = np.array(list(seadata.items()), dtype=[('name', 'U16'), ('value', 'f8')])
|
||||
np.savez(filename, seadata=seanp, iqdata=iqdata)
|
||||
# can be load back via
|
||||
# content = np.load(filename)
|
||||
# content['seadata'], content['iqdata']
|
||||
self.pulselen = self.adq.pulselen
|
||||
times.append(('ana',time.time()))
|
||||
if self.plot:
|
||||
# get reduced interval from adq.sinW
|
||||
pulseint = (self.interval[0], self.interval[0] + self.pulselen)
|
||||
try:
|
||||
self.plotter.plot(
|
||||
self.adq.curves,
|
||||
rois=[pulseint] + [r.interval for r in roilist],
|
||||
average=([r.time for r in roilist],
|
||||
[r.i for r in roilist],
|
||||
[r.q for r in roilist]))
|
||||
except Exception as e:
|
||||
self.log.warning('can not plot %r' % e)
|
||||
else:
|
||||
self.plotter.close()
|
||||
now = time.time()
|
||||
times.append(('plot',now))
|
||||
# print(' '.join('%s %5.3f' % (label, t - self.starttime) for label, t in times))
|
||||
self.starttime = now
|
||||
self.value = freq - self.basefreq
|
||||
for i, roi in enumerate(roilist):
|
||||
roi.i = a = gates[i][0]
|
||||
roi.q = b = gates[i][1]
|
||||
roi.value = math.sqrt(a ** 2 + b ** 2)
|
||||
roi.phase = math.atan2(a,b) * 180 / math.pi
|
||||
inphase = self.roilist[0].i
|
||||
if self.control:
|
||||
newfreq = freq + inphase * self.slope - self.basefreq
|
||||
# step = sorted((-self.maxstep, inphase * self.slope, self.maxstep))[1]
|
||||
if self.old:
|
||||
fdif = freq - self.old[0]
|
||||
idif = inphase - self.old[1]
|
||||
if abs(fdif) >= self.minstep:
|
||||
self.slope = - fdif / idif
|
||||
else:
|
||||
fdif = 0
|
||||
idif = 0
|
||||
newfreq = freq + self.minstep
|
||||
self.old = (freq, inphase)
|
||||
if self.skipctrl > 0: # do no control for some time after changing frequency
|
||||
self.skipctrl -= 1
|
||||
elif self.control:
|
||||
self.freq = sorted((self.freq - self.maxstep, newfreq, self.freq + self.maxstep))[1]
|
||||
#print(times)
|
||||
return Done
|
432
frappy_psi/uniax.py
Normal file
432
frappy_psi/uniax.py
Normal file
@ -0,0 +1,432 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# *****************************************************************************
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Module authors:
|
||||
# M. Zolliker <markus.zolliker@psi.ch>
|
||||
#
|
||||
# *****************************************************************************
|
||||
"""use transducer and motor to adjust force"""
|
||||
|
||||
import time
|
||||
import math
|
||||
from frappy.core import Drivable, Parameter, FloatRange, Done, \
|
||||
Attached, Command, PersistentMixin, PersistentParam, BoolType
|
||||
from frappy.errors import BadValueError, SECoPError
|
||||
from frappy.lib.statemachine import Retry, StateMachine, Restart
|
||||
|
||||
# TODO: fix with new state machine!
|
||||
|
||||
|
||||
class Error(SECoPError):
|
||||
pass
|
||||
|
||||
|
||||
class Uniax(PersistentMixin, Drivable):
|
||||
value = Parameter(unit='N')
|
||||
motor = Attached()
|
||||
transducer = Attached()
|
||||
limit = Parameter('abs limit of force', FloatRange(0, 190, unit='N'), readonly=False, default=150)
|
||||
tolerance = Parameter('force tolerance', FloatRange(0, 10, unit='N'), readonly=False, default=0.2)
|
||||
slope = PersistentParam('spring constant', FloatRange(unit='deg/N'), readonly=False,
|
||||
default=0.5, persistent='auto')
|
||||
pid_i = PersistentParam('integral', FloatRange(), readonly=False, default=0.5, persistent='auto')
|
||||
filter_interval = Parameter('filter time', FloatRange(0, 60, unit='s'), readonly=False, default=5)
|
||||
current_step = Parameter('', FloatRange(unit='deg'), default=0)
|
||||
force_offset = PersistentParam('transducer offset', FloatRange(unit='N'), readonly=False, default=0,
|
||||
initwrite=True, persistent='auto')
|
||||
hysteresis = PersistentParam('force hysteresis', FloatRange(0, 190, unit='N'), readonly=False, default=5,
|
||||
persistent='auto')
|
||||
adjusting = Parameter('', BoolType(), readonly=False, default=False, initwrite=True)
|
||||
adjusting_current = PersistentParam('current when adjusting force', FloatRange(0, 2.8, unit='A'), readonly=False,
|
||||
default=0.5, persistent='auto')
|
||||
safe_step = PersistentParam('max. motor step when adjusting force', FloatRange(0, unit='deg'), readonly=False,
|
||||
default=5, persistent='auto')
|
||||
safe_current = PersistentParam('current when moving far', FloatRange(0, 2.8, unit='A'), readonly=False,
|
||||
default=0.2, persistent='auto')
|
||||
low_pos = Parameter('max. position for positive forces', FloatRange(unit='deg'), readonly=False, needscfg=False)
|
||||
high_pos = Parameter('min. position for negative forces', FloatRange(unit='deg'), readonly=False, needscfg=False)
|
||||
substantial_force = Parameter('min. force change expected within motor play', FloatRange(), default=1)
|
||||
motor_play = Parameter('acceptable motor play within hysteresis', FloatRange(), readonly=False, default=10)
|
||||
motor_max_play = Parameter('acceptable motor play outside hysteresis', FloatRange(), readonly=False, default=90)
|
||||
timeout = Parameter('driving finishes when no progress within this delay', FloatRange(), readonly=False, default=300)
|
||||
pollinterval = 0.1
|
||||
|
||||
_mot_target = None # for detecting manual motor manipulations
|
||||
_filter_start = 0
|
||||
_cnt = 0
|
||||
_sum = 0
|
||||
_cnt_rderr = 0
|
||||
_cnt_wrerr = 0
|
||||
_zero_pos_tol = None
|
||||
_state = None
|
||||
_force = None # raw force
|
||||
|
||||
def earlyInit(self):
|
||||
super().earlyInit()
|
||||
self._zero_pos_tol = {}
|
||||
|
||||
def initModule(self):
|
||||
super().initModule()
|
||||
self._state = StateMachine(logger=self.log, threaded=False, cleanup=self.cleanup)
|
||||
|
||||
def doPoll(self):
|
||||
self.read_value()
|
||||
self._state.cycle()
|
||||
|
||||
def drive_relative(self, step, ntry=3):
|
||||
"""drive relative, try 3 times"""
|
||||
mot = self.motor
|
||||
mot.read_value() # make sure motor value is fresh
|
||||
if self.adjusting and abs(step) > self.safe_step:
|
||||
step = math.copysign(self.safe_step, step)
|
||||
self.current_step = step
|
||||
for _ in range(ntry):
|
||||
try:
|
||||
if abs(mot.value - mot.target) < mot.tolerance:
|
||||
# make sure rounding erros do not suppress small steps
|
||||
newpos = mot.target + step
|
||||
else:
|
||||
newpos = mot.value + step
|
||||
self._mot_target = self.motor.write_target(newpos)
|
||||
self._cnt_wrerr = max(0, self._cnt_wrerr - 1)
|
||||
return True
|
||||
except Exception as e:
|
||||
self.log.warning('drive error %s', e)
|
||||
self._cnt_wrerr += 1
|
||||
if self._cnt_wrerr > 5:
|
||||
raise
|
||||
self.log.warning('motor reset')
|
||||
self.motor.reset()
|
||||
return False
|
||||
|
||||
def motor_busy(self):
|
||||
mot = self.motor
|
||||
if mot.isBusy():
|
||||
if mot.target != self._mot_target:
|
||||
raise Error('control stopped - motor moved directly')
|
||||
return True
|
||||
return False
|
||||
|
||||
def read_value(self):
|
||||
try:
|
||||
self._force = force = self.transducer.read_value()
|
||||
self._cnt_rderr = max(0, self._cnt_rderr - 1)
|
||||
except Exception as e:
|
||||
self._cnt_rderr += 1
|
||||
if self._cnt_rderr > 10:
|
||||
self.stop()
|
||||
self.status = 'ERROR', 'too many read errors: %s' % e
|
||||
self.log.error(self.status[1])
|
||||
self.read_target()
|
||||
return Done
|
||||
|
||||
now = time.time()
|
||||
self._sum += force
|
||||
self._cnt += 1
|
||||
if now < self._filter_start + self.filter_interval:
|
||||
return Done
|
||||
force = self._sum / self._cnt
|
||||
self.reset_filter(now)
|
||||
if abs(force) > self.limit + self.hysteresis:
|
||||
self.motor.stop()
|
||||
self.status = 'ERROR', 'above max limit'
|
||||
self.log.error(self.status[1])
|
||||
self.read_target()
|
||||
return Done
|
||||
if self.zero_pos(force) is None and abs(force) > self.hysteresis:
|
||||
self.set_zero_pos(force, self.motor.read_value())
|
||||
return force
|
||||
|
||||
def reset_filter(self, now=0.0):
|
||||
self._sum = self._cnt = 0
|
||||
self._filter_start = now or time.time()
|
||||
|
||||
def zero_pos(self, value):
|
||||
"""get high_pos or low_pos, depending on sign of value
|
||||
|
||||
:param value: return an estimate for a good starting position
|
||||
"""
|
||||
|
||||
name = 'high_pos' if value > 0 else 'low_pos'
|
||||
if name not in self._zero_pos_tol:
|
||||
return None
|
||||
return getattr(self, name)
|
||||
|
||||
def set_zero_pos(self, force, pos):
|
||||
"""set zero position high_pos or low_pos, depending on sign and value of force
|
||||
|
||||
:param force: the force used for calculating zero_pos
|
||||
:param pos: the position used for calculating zero_pos
|
||||
"""
|
||||
name = 'high_pos' if force > 0 else 'low_pos'
|
||||
if pos is None:
|
||||
self._zero_pos_tol.pop(name, None)
|
||||
return None
|
||||
pos -= force * self.slope
|
||||
tol = (abs(force) - self.hysteresis) * self.slope * 0.2
|
||||
if name in self._zero_pos_tol:
|
||||
old = self.zero_pos(force)
|
||||
if abs(old - pos) < self._zero_pos_tol[name] + tol:
|
||||
return
|
||||
self._zero_pos_tol[name] = tol
|
||||
self.log.info('set %s = %.1f +- %.1f (@%g N)' % (name, pos, tol, force))
|
||||
setattr(self, name, pos)
|
||||
|
||||
def cleanup(self, state):
|
||||
"""in case of error, set error status"""
|
||||
if state.stopped: # stop or restart
|
||||
if state.stopped is Restart:
|
||||
return
|
||||
self.status = 'IDLE', 'stopped'
|
||||
self.log.warning('stopped')
|
||||
else:
|
||||
self.status = 'ERROR', str(state.last_error)
|
||||
if isinstance(state.last_error, Error):
|
||||
self.log.error('%s', state.last_error)
|
||||
else:
|
||||
self.log.error('%r raised in state %r', str(state.last_error), state.status_string)
|
||||
self.read_target() # make target invalid
|
||||
self.motor.stop()
|
||||
self.write_adjusting(False)
|
||||
|
||||
def reset_progress(self, state):
|
||||
state.prev_force = self.value
|
||||
state.prev_pos = self.motor.value
|
||||
state.prev_time = time.time()
|
||||
|
||||
def check_progress(self, state):
|
||||
force_step = self.target - self.value
|
||||
direction = math.copysign(1, force_step)
|
||||
try:
|
||||
force_progress = direction * (self.value - state.prev_force)
|
||||
except AttributeError: # prev_force undefined?
|
||||
self.reset_progress(state)
|
||||
return True
|
||||
if force_progress >= self.substantial_force:
|
||||
self.reset_progress(state)
|
||||
else:
|
||||
motor_dif = abs(self.motor.value - state.prev_pos)
|
||||
if motor_dif > self.motor_play:
|
||||
if motor_dif > self.motor_max_play:
|
||||
raise Error('force seems not to change substantially %g %g (%g %g)' % (self.value, self.motor.value, state.prev_force, state.prev_pos))
|
||||
return False
|
||||
return True
|
||||
|
||||
def adjust(self, state):
|
||||
"""adjust force"""
|
||||
if state.init:
|
||||
state.phase = 0 # just initialized
|
||||
state.in_since = 0
|
||||
state.direction = math.copysign(1, self.target - self.value)
|
||||
state.pid_fact = 1
|
||||
if self.motor_busy():
|
||||
return Retry()
|
||||
self.value = self._force
|
||||
force_step = self.target - self.value
|
||||
if abs(force_step) < self.tolerance:
|
||||
if state.in_since == 0:
|
||||
state.in_since = state.now
|
||||
if state.now > state.in_since + 10:
|
||||
return self.within_tolerance
|
||||
else:
|
||||
if force_step * state.direction < 0:
|
||||
if state.pid_fact == 1:
|
||||
self.log.info('overshoot -> adjust with reduced pid_i')
|
||||
state.pid_fact = 0.1
|
||||
state.in_since = 0
|
||||
if state.phase == 0:
|
||||
state.phase = 1
|
||||
self.reset_progress(state)
|
||||
self.write_adjusting(True)
|
||||
self.status = 'BUSY', 'adjusting force'
|
||||
elif not self.check_progress(state):
|
||||
if abs(self.value) < self.hysteresis:
|
||||
if motor_dif > self.motor_play:
|
||||
self.log.warning('adjusting failed - try to find zero pos')
|
||||
self.set_zero_pos(self.target, None)
|
||||
return self.find
|
||||
elif time.time() > state.prev_time + self.timeout:
|
||||
if state.phase == 1:
|
||||
state.phase = 2
|
||||
self.log.warning('no substantial progress since %d sec', self.timeout)
|
||||
self.status = 'IDLE', 'adjusting timeout'
|
||||
self.drive_relative(force_step * self.slope * self.pid_i * min(1, state.delta()) * state.pid_fact)
|
||||
return Retry()
|
||||
|
||||
def within_tolerance(self, state):
|
||||
"""within tolerance"""
|
||||
if state.init:
|
||||
self.status = 'IDLE', 'within tolerance'
|
||||
return Retry()
|
||||
if self.motor_busy():
|
||||
return Retry()
|
||||
force_step = self.target - self.value
|
||||
if abs(force_step) < self.tolerance * 0.5:
|
||||
self.current_step = 0
|
||||
else:
|
||||
self.check_progress(state)
|
||||
self.drive_relative(force_step * self.slope * self.pid_i * min(1, state.delta()) * 0.1)
|
||||
if abs(force_step) > self.tolerance:
|
||||
return self.out_of_tolerance
|
||||
return Retry()
|
||||
|
||||
def out_of_tolerance(self, state):
|
||||
"""out of tolerance"""
|
||||
if state.init:
|
||||
self.status = 'WARN', 'out of tolerance'
|
||||
state.in_since = 0
|
||||
return Retry()
|
||||
if self.motor_busy():
|
||||
return Retry()
|
||||
force_step = self.target - self._force
|
||||
if abs(force_step) < self.tolerance:
|
||||
if state.in_since == 0:
|
||||
state.in_since = state.now
|
||||
if state.now > state.in_since + 10:
|
||||
return self.within_tolerance
|
||||
if abs(force_step) < self.tolerance * 0.5:
|
||||
return Retry()
|
||||
self.check_progress(state)
|
||||
self.drive_relative(force_step * self.slope * self.pid_i * min(1, state.delta()) * 0.1)
|
||||
return Retry()
|
||||
|
||||
def find(self, state):
|
||||
"""find active (engaged) range"""
|
||||
if state.init:
|
||||
state.prev_direction = 0 # find not yet started
|
||||
self.reset_progress(state)
|
||||
direction = math.copysign(1, self.target)
|
||||
self.value = self._force
|
||||
abs_force = self.value * direction
|
||||
if abs_force > self.hysteresis or abs_force > self.target * direction:
|
||||
if self.motor_busy():
|
||||
self.log.info('motor stopped - substantial force detected: %g', self.value)
|
||||
self.motor.stop()
|
||||
elif state.prev_direction == 0:
|
||||
return self.adjust
|
||||
if abs_force > self.hysteresis:
|
||||
self.set_zero_pos(self.value, self.motor.read_value())
|
||||
return self.adjust
|
||||
if abs_force < -self.hysteresis:
|
||||
state.force_before_free = self.value
|
||||
return self.free
|
||||
if self.motor_busy():
|
||||
if direction == -state.prev_direction: # target direction changed
|
||||
self.motor.stop()
|
||||
state.init_find = True # restart find
|
||||
return Retry()
|
||||
zero_pos = self.zero_pos(self.target)
|
||||
if state.prev_direction: # find already started
|
||||
if abs(self.motor.target - self.motor.value) > self.motor.tolerance:
|
||||
# no success on last find try, try short and strong step
|
||||
self.write_adjusting(True)
|
||||
self.log.info('one step to %g', self.motor.value + self.safe_step)
|
||||
self.drive_relative(direction * self.safe_step)
|
||||
return Retry()
|
||||
else:
|
||||
state.prev_direction = math.copysign(1, self.target)
|
||||
side_name = 'negative' if direction == -1 else 'positive'
|
||||
if zero_pos is not None:
|
||||
self.status = 'BUSY', 'change to %s side' % side_name
|
||||
zero_pos += direction * (self.hysteresis * self.slope - self.motor.tolerance)
|
||||
if (self.motor.value - zero_pos) * direction < -self.motor.tolerance:
|
||||
self.write_adjusting(False)
|
||||
self.log.info('change side to %g', zero_pos)
|
||||
self.drive_relative(zero_pos - self.motor.value)
|
||||
return Retry()
|
||||
# we are already at or beyond zero_pos
|
||||
return self.adjust
|
||||
self.write_adjusting(False)
|
||||
self.status = 'BUSY', 'find %s side' % side_name
|
||||
self.log.info('one turn to %g', self.motor.value + direction * 360)
|
||||
self.drive_relative(direction * 360)
|
||||
return Retry()
|
||||
|
||||
def free(self, state):
|
||||
"""free from high force at other end"""
|
||||
if state.init:
|
||||
state.free_way = None
|
||||
self.reset_progress(state)
|
||||
if self.motor_busy():
|
||||
return Retry()
|
||||
self.value = self._force
|
||||
if abs(self.value) > abs(state.force_before_free) + self.hysteresis:
|
||||
raise Error('force increase while freeing')
|
||||
if abs(self.value) < self.hysteresis:
|
||||
return self.find
|
||||
if state.free_way is None:
|
||||
state.free_way = 0
|
||||
self.log.info('free from high force %g', self.value)
|
||||
self.write_adjusting(True)
|
||||
direction = math.copysign(1, self.target)
|
||||
if state.free_way > abs(state.force_before_free + self.hysteresis) * self.slope + self.motor_max_play:
|
||||
raise Error('freeing failed')
|
||||
state.free_way += self.safe_step
|
||||
self.drive_relative(direction * self.safe_step)
|
||||
return Retry()
|
||||
|
||||
def write_target(self, target):
|
||||
if abs(target) > self.limit:
|
||||
raise BadValueError('force above limit')
|
||||
if abs(target - self.value) <= self.tolerance:
|
||||
if not self.isBusy():
|
||||
self.status = 'IDLE', 'already at target'
|
||||
self._state.start(self.within_tolerance)
|
||||
return target
|
||||
self.log.info('new target %g', target)
|
||||
self._cnt_rderr = 0
|
||||
self._cnt_wrerr = 0
|
||||
self.status = 'BUSY', 'changed target'
|
||||
self.target = target
|
||||
if self.value * math.copysign(1, target) > self.hysteresis:
|
||||
self._state.start(self.adjust)
|
||||
else:
|
||||
self._state.start(self.find)
|
||||
return Done
|
||||
|
||||
def read_target(self):
|
||||
if self._state.state is None:
|
||||
if self.status[1]:
|
||||
raise Error(self.status[1])
|
||||
raise Error('inactive')
|
||||
return self.target
|
||||
|
||||
@Command()
|
||||
def stop(self):
|
||||
if self.motor.isBusy():
|
||||
self.log.info('stop motor')
|
||||
self.motor.stop()
|
||||
self.status = 'IDLE', 'stopped'
|
||||
self._state.stop()
|
||||
|
||||
def write_force_offset(self, value):
|
||||
self.force_offset = value
|
||||
self.transducer.write_offset(value)
|
||||
return Done
|
||||
|
||||
def write_adjusting(self, value):
|
||||
mot = self.motor
|
||||
if value:
|
||||
mot_current = self.adjusting_current
|
||||
mot.write_move_limit(self.safe_step)
|
||||
else:
|
||||
mot_current = self.safe_current
|
||||
mot.write_safe_current(mot_current)
|
||||
if abs(mot_current - mot.maxcurrent) > 0.01: # resolution of current: 2.8 / 250
|
||||
self.log.info('motor current %g', mot_current)
|
||||
mot.write_maxcurrent(mot_current)
|
||||
return value
|
89
frappy_psi/vector.py
Normal file
89
frappy_psi/vector.py
Normal file
@ -0,0 +1,89 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# *****************************************************************************
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Module authors:
|
||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||
# *****************************************************************************
|
||||
"""generic 3D vector"""
|
||||
|
||||
from frappy.core import Attached, Drivable, Readable, Parameter, Done
|
||||
from frappy.datatypes import FloatRange, TupleOf, StatusType, Enum
|
||||
|
||||
|
||||
class VectorRd(Readable):
|
||||
"""generic readable vector"""
|
||||
value = Parameter(datatype=TupleOf(FloatRange(), FloatRange(), FloatRange()))
|
||||
x = Attached()
|
||||
y = Attached()
|
||||
z = Attached()
|
||||
pollFuncs = None
|
||||
components = None
|
||||
|
||||
def initModule(self):
|
||||
super().initModule()
|
||||
members = []
|
||||
status_codes = {} # collect all possible status codes
|
||||
components = []
|
||||
for name in 'xyz':
|
||||
component = getattr(self, name)
|
||||
members.append(component.parameters['value'].datatype.copy())
|
||||
components.append(component)
|
||||
for code in component.status[0].enum.members:
|
||||
status_codes[int(code)] = code.name
|
||||
self.parameters['value'].datatype = TupleOf(*members)
|
||||
self.parameters['status'].datatype = StatusType(Enum(
|
||||
'status', **{k: v for v, k in status_codes.items()}))
|
||||
self.components = components
|
||||
|
||||
def doPoll(self):
|
||||
for component in self.components:
|
||||
component.doPoll()
|
||||
# update
|
||||
component.pollInfo.last_main = self.pollInfo.last_main
|
||||
self.value = self.merge_value()
|
||||
self.status = self.merge_status()
|
||||
|
||||
def merge_value(self):
|
||||
return [c.value for c in self.components]
|
||||
|
||||
def merge_status(self):
|
||||
status = -1, ''
|
||||
for c in self.components:
|
||||
if c.status[0] > status[0]:
|
||||
status = c.status
|
||||
return status
|
||||
|
||||
def read_value(self):
|
||||
return tuple((c.read_value() for c in self.components))
|
||||
|
||||
def read_status(self):
|
||||
[c.read_status() for c in self.components]
|
||||
return self.merge_status()
|
||||
|
||||
|
||||
class Vector(Drivable, VectorRd):
|
||||
"""generic drivable vector"""
|
||||
target = Parameter(datatype=TupleOf(FloatRange(), FloatRange(), FloatRange()))
|
||||
|
||||
def initModule(self):
|
||||
super().initModule()
|
||||
members = []
|
||||
for component in self.components:
|
||||
members.append(component.parameters['target'].datatype.copy())
|
||||
self.parameters['target'].datatype = TupleOf(*members)
|
||||
|
||||
def write_target(self, value):
|
||||
return tuple((c.write_target(v) for v, c in zip(value, self.components)))
|
Loading…
x
Reference in New Issue
Block a user