add missing files from secop_psi

- all of them have to be checked!

Change-Id: I89d55ca683d0b2710222f14c2c3cd42f8fbf3a1f
This commit is contained in:
zolliker 2023-05-03 11:24:47 +02:00
parent 31be98c62e
commit bbe70fb3cb
22 changed files with 5352 additions and 0 deletions

294
frappy_psi/adq_mr.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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)))