further work on needle valve, pump and lakeshore
This commit is contained in:
parent
19571ab83d
commit
41b51b35fd
@ -7,10 +7,10 @@ Node('ori7test.psi.ch',
|
|||||||
|
|
||||||
rack = Rack(Mod)
|
rack = Rack(Mod)
|
||||||
|
|
||||||
with rack.lakeshore() as ls:
|
rack.lakeshore()
|
||||||
ls.sensor('Ts', channel='C', calcurve='x186350')
|
rack.sensor('Ts', channel='C', calcurve='x186350')
|
||||||
ls.loop('T', channel='B', calcurve='x174786')
|
rack.loop('T', channel='B', calcurve='x174786', output_module='htr', target=10)
|
||||||
ls.heater('htr', '100W', 100)
|
rack.heater('htr', 1, '100W', 25)
|
||||||
|
|
||||||
rack.ccu(he=True, n2=True)
|
rack.ccu(he=True, n2=True)
|
||||||
|
|
||||||
|
@ -215,7 +215,10 @@ class HasStates:
|
|||||||
self.read_status()
|
self.read_status()
|
||||||
if fast_poll:
|
if fast_poll:
|
||||||
sm.reset_fast_poll = True
|
sm.reset_fast_poll = True
|
||||||
self.setFastPoll(True)
|
if fast_poll is True:
|
||||||
|
self.setFastPoll(True)
|
||||||
|
else:
|
||||||
|
self.setFastPoll(True, fast_poll)
|
||||||
self.pollInfo.trigger(True) # trigger poller
|
self.pollInfo.trigger(True) # trigger poller
|
||||||
|
|
||||||
def stop_machine(self, stopped_status=(IDLE, 'stopped')):
|
def stop_machine(self, stopped_status=(IDLE, 'stopped')):
|
||||||
|
131
frappy_psi/autofill.py
Normal file
131
frappy_psi/autofill.py
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
# *****************************************************************************
|
||||||
|
#
|
||||||
|
# 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 time
|
||||||
|
from frappy.core import Attached, Readable, Writable, Parameter, Command, \
|
||||||
|
IDLE, BUSY, DISABLED, ERROR
|
||||||
|
from frappy.datatypes import FloatRange, StatusType, TupleOf, EnumType
|
||||||
|
from frappy.states import HasStates, Retry, status_code
|
||||||
|
|
||||||
|
|
||||||
|
class AutoFill(HasStates, Readable):
|
||||||
|
level = Attached(Readable)
|
||||||
|
valve = Attached(Writable)
|
||||||
|
status = Parameter(datatype=StatusType(Readable, 'BUSY'))
|
||||||
|
mode = Parameter('auto mode', EnumType(disabled=0, auto=30), readonly=False)
|
||||||
|
|
||||||
|
fill_level = Parameter('low threshold triggering start filling',
|
||||||
|
FloatRange(unit='%'), readonly=False)
|
||||||
|
full_level = Parameter('high threshold triggering stop filling',
|
||||||
|
FloatRange(unit='%'), readonly=False)
|
||||||
|
fill_minutes_range = Parameter('range of possible fill rate',
|
||||||
|
TupleOf(FloatRange(unit='min'), FloatRange(unit='min')),
|
||||||
|
readonly=False)
|
||||||
|
hold_hours_range = Parameter('range of possible consumption rate',
|
||||||
|
TupleOf(FloatRange(unit='h'), FloatRange(unit='h')),
|
||||||
|
readonly=False)
|
||||||
|
fill_delay = Parameter('delay for cooling the transfer line',
|
||||||
|
FloatRange(unit='min'), readonly=False)
|
||||||
|
|
||||||
|
def read_status(self):
|
||||||
|
if self.mode == 'DISABLED':
|
||||||
|
return DISABLED, ''
|
||||||
|
vstatus = self.valve.status
|
||||||
|
if vstatus[0] // 100 != IDLE // 100:
|
||||||
|
self.stop_machine(vstatus)
|
||||||
|
return vstatus
|
||||||
|
status = self.level.read_status(self)
|
||||||
|
if status[0] // 100 == IDLE // 100:
|
||||||
|
return HasStates.read_status(self)
|
||||||
|
self.stop_machine(status)
|
||||||
|
return status
|
||||||
|
|
||||||
|
def write_mode(self, mode):
|
||||||
|
if mode == 'DISABLED':
|
||||||
|
self.stop_machine((DISABLED, ''))
|
||||||
|
elif mode == 'AUTO':
|
||||||
|
self.start_machine(self.watching)
|
||||||
|
return mode
|
||||||
|
|
||||||
|
@status_code(BUSY)
|
||||||
|
def watching(self, state):
|
||||||
|
if state.init:
|
||||||
|
self.valve.write_target(0)
|
||||||
|
delta = state.delta(10)
|
||||||
|
raw = self.level.value
|
||||||
|
if raw > self.value:
|
||||||
|
self.value -= delta / (3600 * self.hold_hours_range[1])
|
||||||
|
elif raw < self.value:
|
||||||
|
self.value -= delta / (3600 * self.hold_hours_range[0])
|
||||||
|
else:
|
||||||
|
self.value = raw
|
||||||
|
if self.value < self.fill_level:
|
||||||
|
return self.precooling
|
||||||
|
return Retry
|
||||||
|
|
||||||
|
@status_code(BUSY)
|
||||||
|
def precooling(self, state):
|
||||||
|
if state.init:
|
||||||
|
state.fillstart = state.now
|
||||||
|
self.valve.write_target(1)
|
||||||
|
delta = state.delta(1)
|
||||||
|
raw = self.level.value
|
||||||
|
if raw > self.value:
|
||||||
|
self.value += delta / (60 * self.fill_minutes_range[0])
|
||||||
|
elif raw < self.value:
|
||||||
|
self.value -= delta / (60 * self.fill_minutes_range[0])
|
||||||
|
else:
|
||||||
|
self.value = raw
|
||||||
|
if self.value > self.full_level:
|
||||||
|
return self.watching
|
||||||
|
if state.now > state.fillstart + self.fill_delay * 60:
|
||||||
|
return self.filling
|
||||||
|
return Retry
|
||||||
|
|
||||||
|
@status_code(BUSY)
|
||||||
|
def filling(self, state):
|
||||||
|
delta = state.delta(1)
|
||||||
|
raw = self.level.value
|
||||||
|
if raw > self.value:
|
||||||
|
self.value += delta / (60 * self.fill_minutes_range[0])
|
||||||
|
elif raw < self.value:
|
||||||
|
self.value += delta / (60 * self.fill_minutes_range[1])
|
||||||
|
else:
|
||||||
|
self.value = raw
|
||||||
|
if self.value > self.full_level:
|
||||||
|
return self.watching
|
||||||
|
return Retry
|
||||||
|
|
||||||
|
def on_cleanup(self, state):
|
||||||
|
try:
|
||||||
|
self.valve.write_target(0)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
super().on_cleanup()
|
||||||
|
|
||||||
|
@Command()
|
||||||
|
def fill(self):
|
||||||
|
self.mode = 'AUTO'
|
||||||
|
self.start_machine(self.precooling, fillstart=time.time())
|
||||||
|
|
||||||
|
@Command()
|
||||||
|
def stop(self):
|
||||||
|
self.start_machine(self.watching)
|
128
frappy_psi/butterflyvalve.py
Normal file
128
frappy_psi/butterflyvalve.py
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
# *****************************************************************************
|
||||||
|
#
|
||||||
|
# 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>
|
||||||
|
#
|
||||||
|
# *****************************************************************************
|
||||||
|
|
||||||
|
from frappy.core import Attached, Command, EnumType, FloatRange, \
|
||||||
|
Drivable, Parameter, BUSY, IDLE, ERROR
|
||||||
|
|
||||||
|
|
||||||
|
class Valve(Drivable):
|
||||||
|
motor = Attached(Drivable) # refers to motor module
|
||||||
|
value = Parameter('valve state',
|
||||||
|
EnumType(closed=0, open=1, undefined=2),
|
||||||
|
default=2)
|
||||||
|
status = Parameter() # inherit properties from Drivable
|
||||||
|
target = Parameter('valve target',
|
||||||
|
EnumType(closed=0, open=1),
|
||||||
|
readonly=False)
|
||||||
|
# TODO: convert to properties after tests
|
||||||
|
open_pos = Parameter('target position for open state', FloatRange(), readonly=False, default=80)
|
||||||
|
mid_pos = Parameter('position for changing speed', FloatRange(), readonly=False, default=5)
|
||||||
|
fast_speed = Parameter('normal speed', FloatRange(), readonly=False, default=40)
|
||||||
|
slow_speed = Parameter('reduced speed', FloatRange(), readonly=False, default=10)
|
||||||
|
__motor_target = None
|
||||||
|
__status = IDLE, ''
|
||||||
|
__value = 'undefined'
|
||||||
|
__drivestate = 0 # 2 when driving to intermediate target or on retry, 1 when driving to final target, 0 when idle
|
||||||
|
|
||||||
|
def doPoll(self):
|
||||||
|
mot = self.motor
|
||||||
|
motpos = mot.read_value()
|
||||||
|
scode, stext = mot.read_status()
|
||||||
|
drivestate = self.__drivestate
|
||||||
|
if scode >= ERROR:
|
||||||
|
if self.__drivestate and self.__remaining_tries > 0:
|
||||||
|
drivestate = 2
|
||||||
|
self.__remaining_tries -= 1
|
||||||
|
mot.reset()
|
||||||
|
mot.write_speed(self.slow_speed)
|
||||||
|
self.__status = BUSY, f'retry {self._action}'
|
||||||
|
else:
|
||||||
|
self.__status = ERROR, f'valve motor: {stext}'
|
||||||
|
elif scode < BUSY:
|
||||||
|
if self.__motor_target is not None and mot.target != self.__motor_target:
|
||||||
|
self.__status = ERROR, 'motor was driven directly'
|
||||||
|
elif drivestate == 2:
|
||||||
|
self.goto(self.target)
|
||||||
|
drivestate = 1
|
||||||
|
else:
|
||||||
|
if -3 < motpos < 3:
|
||||||
|
self.__value = 'closed'
|
||||||
|
self.__status = IDLE, ''
|
||||||
|
elif self.open_pos * 0.5 < motpos < self.open_pos * 1.5:
|
||||||
|
self.__value = 'open'
|
||||||
|
self.__status = IDLE, ''
|
||||||
|
else:
|
||||||
|
self.__status = ERROR, 'undefined'
|
||||||
|
if self.__drivestate and not self.isBusy(self.__status):
|
||||||
|
drivestate = 0
|
||||||
|
self.__motor_target = None
|
||||||
|
self.setFastPoll(False)
|
||||||
|
self.__drivestate = drivestate
|
||||||
|
self.read_status()
|
||||||
|
self.read_value()
|
||||||
|
|
||||||
|
def read_status(self):
|
||||||
|
return self.__status
|
||||||
|
|
||||||
|
def read_value(self):
|
||||||
|
if self.read_status()[0] >= BUSY:
|
||||||
|
return 'undefined'
|
||||||
|
return self.__value
|
||||||
|
|
||||||
|
def goto(self, target):
|
||||||
|
"""go to open, closed or intermediate position
|
||||||
|
|
||||||
|
the intermediate position is targeted when a speed change is needed
|
||||||
|
|
||||||
|
return 2 when a retry is needed, 1 else
|
||||||
|
"""
|
||||||
|
mot = self.motor
|
||||||
|
if target: # 'open'
|
||||||
|
self._action = 'opening'
|
||||||
|
if True or mot.value > self.mid_pos:
|
||||||
|
mot.write_speed(self.fast_speed)
|
||||||
|
self.__motor_target = mot.write_target(self.open_pos)
|
||||||
|
return 1
|
||||||
|
mot.write_speed(self.slow_speed)
|
||||||
|
self.__motor_target = mot.write_target(self.mid_pos)
|
||||||
|
return 2
|
||||||
|
self._action = 'closing'
|
||||||
|
if mot.value > self.mid_pos * 2:
|
||||||
|
mot.write_speed(self.fast_speed)
|
||||||
|
self.__motor_target = mot.write_target(self.mid_pos)
|
||||||
|
return 2
|
||||||
|
mot.write_speed(self.slow_speed)
|
||||||
|
self.__motor_target = mot.write_target(0)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
def write_target(self, target):
|
||||||
|
self.__remaining_tries = 5
|
||||||
|
self.__drivestate = self.goto(target)
|
||||||
|
self.__status = BUSY, self._action
|
||||||
|
self.read_status()
|
||||||
|
self.read_value()
|
||||||
|
self.setFastPoll(True)
|
||||||
|
|
||||||
|
@Command() # python decorator to mark it as a command
|
||||||
|
def stop(self):
|
||||||
|
"""stop the motor -> value might get undefined"""
|
||||||
|
self.__drivestate = 0
|
||||||
|
self.motor.stop()
|
@ -1,83 +1,105 @@
|
|||||||
import os
|
import os
|
||||||
|
from glob import glob
|
||||||
|
from pathlib import Path
|
||||||
from configparser import ConfigParser
|
from configparser import ConfigParser
|
||||||
|
from frappy.errors import ConfigError
|
||||||
|
|
||||||
|
|
||||||
class Lsc:
|
class Rack:
|
||||||
def __init__(self, modfactory, ls_uri, ls_ioname='lsio', ls_devname='ls', ls_model='336', **kwds):
|
configbase = Path('/home/l_samenv/.config/frappy_instruments')
|
||||||
self.modfactory = Mod = modfactory
|
|
||||||
self.model = ls_model
|
def __init__(self, modfactory, **kwds):
|
||||||
self.ioname = ls_ioname
|
self.modfactory = modfactory
|
||||||
self.devname = ls_devname
|
instpath = self.configbase / os.environ['Instrument']
|
||||||
self.io = Mod(self.ioname, cls=f'frappy_psi.lakeshore.IO{self.model}',
|
sections = {}
|
||||||
description='comm. to lakeshore in cc rack',
|
self.config = {}
|
||||||
uri=ls_uri)
|
files = glob(str(instpath / '*.ini'))
|
||||||
self.dev = Mod(self.devname, cls=f'frappy_psi.lakeshore.Device{self.model}',
|
for filename in files:
|
||||||
description='lakeshore in cc rack', io=self.ioname, curve_handling=True)
|
parser = ConfigParser()
|
||||||
self.loops = {}
|
parser.optionxform = str
|
||||||
self.outputs = {}
|
parser.read([filename])
|
||||||
|
for section in parser.sections():
|
||||||
|
prev = sections.get(section)
|
||||||
|
if prev:
|
||||||
|
raise ConfigError(f'duplicate {section} section in {filename} and {prev}')
|
||||||
|
sections[section] = filename
|
||||||
|
self.config.update(parser.items(section))
|
||||||
|
if 'rack' not in sections:
|
||||||
|
raise ConfigError(f'no rack found in {instpath}')
|
||||||
|
self.props = {} # dict (<property>, <method>) of value
|
||||||
|
self.mods = {} # dict (<property>, <method>) of list of <cfg>
|
||||||
|
|
||||||
|
def set_props(self, mod, **kwds):
|
||||||
|
for prop, method in kwds.items():
|
||||||
|
value = self.props.get((prop, method))
|
||||||
|
if value is None:
|
||||||
|
# add mod to the list of cfgs to be fixed
|
||||||
|
self.mods.setdefault((prop, method), []).append(mod)
|
||||||
|
else:
|
||||||
|
# set prop in current module
|
||||||
|
if not mod.get(prop): # do not override given and not empty property
|
||||||
|
mod[prop] = value
|
||||||
|
|
||||||
|
def fix_props(self, method, **kwds):
|
||||||
|
for prop, value in kwds.items():
|
||||||
|
if (prop, method) in self.props:
|
||||||
|
raise ConfigError(f'duplicate call to {method}()')
|
||||||
|
self.props[prop, method] = value
|
||||||
|
# set property in modules to be fixed
|
||||||
|
for mod in self.mods.get((prop, method), ()):
|
||||||
|
mod[prop] = value
|
||||||
|
|
||||||
|
def lakeshore(self, ls_uri=None, io='ls_io', dev='ls', model='336', **kwds):
|
||||||
|
Mod = self.modfactory
|
||||||
|
self.fix_props('lakeshore', io=io, device=dev)
|
||||||
|
self.ls_model = model
|
||||||
|
self.ls_dev = dev
|
||||||
|
ls_uri = ls_uri or self.config.get('ls_uri')
|
||||||
|
Mod(io, cls=f'frappy_psi.lakeshore.IO{self.ls_model}',
|
||||||
|
description='comm. to lakeshore in cc rack', uri=ls_uri)
|
||||||
|
self.dev = Mod(dev, cls=f'frappy_psi.lakeshore.Device{self.ls_model}',
|
||||||
|
description='lakeshore in cc rack', io=io, curve_handling=True)
|
||||||
|
|
||||||
def sensor(self, name, channel, calcurve, **kwds):
|
def sensor(self, name, channel, calcurve, **kwds):
|
||||||
Mod = self.modfactory
|
Mod = self.modfactory
|
||||||
kwds.setdefault('cls', f'frappy_psi.lakeshore.Sensor{self.model}')
|
kwds.setdefault('cls', f'frappy_psi.lakeshore.Sensor{self.ls_model}')
|
||||||
kwds.setdefault('description', f'T sensor {name}')
|
kwds.setdefault('description', f'T sensor {name}')
|
||||||
return Mod(name, channel=channel, calcurve=calcurve,
|
|
||||||
io=self.ioname, device=self.devname, **kwds)
|
|
||||||
|
|
||||||
def loop(self, name, channel, calcurve, **kwds):
|
|
||||||
Mod = self.modfactory
|
|
||||||
kwds.setdefault('cls', f'frappy_psi.lakeshore.Loop{self.model}')
|
|
||||||
kwds.setdefault('description', f'T loop {name}')
|
|
||||||
mod = Mod(name, channel=channel, calcurve=calcurve,
|
mod = Mod(name, channel=channel, calcurve=calcurve,
|
||||||
io=self.ioname, device=self.devname, **kwds)
|
device=self.ls_dev, **kwds)
|
||||||
self.loops[name] = mod
|
self.set_props(mod, io='lakeshore', dev='lakeshore')
|
||||||
return mod
|
|
||||||
|
|
||||||
def heater(self, name, max_heater, resistance, output_no=1, **kwds):
|
def loop(self, name, channel, calcurve, output_module, **kwds):
|
||||||
|
Mod = self.modfactory
|
||||||
|
kwds.setdefault('cls', f'frappy_psi.lakeshore.Loop{self.ls_model}')
|
||||||
|
kwds.setdefault('description', f'T loop {name}')
|
||||||
|
Mod(name, channel=channel, calcurve=calcurve, output_module=output_module,
|
||||||
|
device=self.ls_dev, **kwds)
|
||||||
|
self.fix_props(f'heater({output_module})', description=f'heater for {name}')
|
||||||
|
|
||||||
|
def heater(self, name, output_no, max_heater, resistance, **kwds):
|
||||||
Mod = self.modfactory
|
Mod = self.modfactory
|
||||||
if output_no == 1:
|
if output_no == 1:
|
||||||
kwds.setdefault('cls', f'frappy_psi.lakeshore.MainOutput{self.model}')
|
kwds.setdefault('cls', f'frappy_psi.lakeshore.MainOutput{self.ls_model}')
|
||||||
elif output_no == 2:
|
elif output_no == 2:
|
||||||
kwds.setdefault('cls', f'frappy_psi.lakeshore.SecondaryOutput{self.model}')
|
kwds.setdefault('cls', f'frappy_psi.lakeshore.SecondaryOutput{self.ls_model}')
|
||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
kwds.setdefault('description', '')
|
kwds.setdefault('description', '')
|
||||||
mod = Mod(name, max_heater=max_heater, resistance=resistance,
|
mod = Mod(name, max_heater=max_heater, resistance=resistance, **kwds)
|
||||||
io=self.ioname, device=self.devname, **kwds)
|
self.set_props(mod, io='lakeshore', device='lakeshore', description=f'heater({name})')
|
||||||
self.outputs[name] = mod
|
|
||||||
return mod
|
|
||||||
|
|
||||||
def __enter__(self):
|
def ccu(self, ccu_uri=None, ccu_io='ccu_io', he=None, n2=None, **kwds):
|
||||||
return self
|
Mod = self.modfactory
|
||||||
|
self.ccu_io = ccu_io
|
||||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
ccu_uri = ccu_uri or self.config.get('ccu_uri')
|
||||||
outmodules = dict(self.outputs)
|
# self.devname = ccu_devname
|
||||||
for name, loop in self.loops.items():
|
Mod(ccu_io, 'frappy_psi.ccu4.IO',
|
||||||
outname = loop.get('output_module')
|
|
||||||
if outname:
|
|
||||||
out = outmodules.pop(outname, None)
|
|
||||||
if not out:
|
|
||||||
raise KeyError(f'{outname} is not a output module in this lakeshore')
|
|
||||||
else:
|
|
||||||
if not outmodules:
|
|
||||||
raise KeyError(f'{name} needs an output module on this lakeshore')
|
|
||||||
outname = list(outmodules)[0]
|
|
||||||
out = outmodules.pop(outname)
|
|
||||||
loop['output_module'] = outname
|
|
||||||
if not out['description']:
|
|
||||||
out['description'] = f'heater for {outname}'
|
|
||||||
|
|
||||||
|
|
||||||
class CCU:
|
|
||||||
def __init__(self, Mod, ccu_uri, ccu_ioname='ccuio', ccu_devname='ccu', he=None, n2=None, **kwds):
|
|
||||||
self.ioname = ccu_ioname
|
|
||||||
self.devname = ccu_devname
|
|
||||||
Mod(self.ioname, 'frappy_psi.ccu4.CCU4IO',
|
|
||||||
'comm. to CCU4', uri=ccu_uri)
|
'comm. to CCU4', uri=ccu_uri)
|
||||||
if he:
|
if he:
|
||||||
if not isinstance(he, str): # e.g. True
|
if not isinstance(he, str): # e.g. True
|
||||||
he = 'He_lev'
|
he = 'He_lev'
|
||||||
Mod(he, cls='frappy_psi.ccu4.HeLevel',
|
Mod(he, cls='frappy_psi.ccu4.HeLevel',
|
||||||
description='the He Level', io=self.ioname)
|
description='the He Level', io=self.ccu_io)
|
||||||
if n2:
|
if n2:
|
||||||
if isinstance(n2, str):
|
if isinstance(n2, str):
|
||||||
n2 = n2.split(',')
|
n2 = n2.split(',')
|
||||||
@ -86,40 +108,32 @@ class CCU:
|
|||||||
n2, valve, upper, lower = n2 + ['N2_lev', 'N2_valve', 'N2_upper', 'N2_lower'][len(n2):]
|
n2, valve, upper, lower = n2 + ['N2_lev', 'N2_valve', 'N2_upper', 'N2_lower'][len(n2):]
|
||||||
print(n2, valve, upper, lower)
|
print(n2, valve, upper, lower)
|
||||||
Mod(n2, cls='frappy_psi.ccu4.N2Level',
|
Mod(n2, cls='frappy_psi.ccu4.N2Level',
|
||||||
description='the N2 Level', io=self.ioname,
|
description='the N2 Level', io=ccu_io,
|
||||||
valve=valve, upper=upper, lower=lower)
|
valve=valve, upper=upper, lower=lower)
|
||||||
Mod(valve, cls='frappy_psi.ccu4.N2FillValve',
|
Mod(valve, cls='frappy_psi.ccu4.N2FillValve',
|
||||||
description='LN2 fill valve', io=self.ioname)
|
description='LN2 fill valve', io=ccu_io)
|
||||||
Mod(upper, cls='frappy_psi.ccu4.N2TempSensor',
|
Mod(upper, cls='frappy_psi.ccu4.N2TempSensor',
|
||||||
description='upper LN2 sensor')
|
description='upper LN2 sensor')
|
||||||
Mod(lower, cls='frappy_psi.ccu4.N2TempSensor',
|
Mod(lower, cls='frappy_psi.ccu4.N2TempSensor',
|
||||||
description='lower LN2 sensor')
|
description='lower LN2 sensor')
|
||||||
|
|
||||||
|
def hepump(self, hepump_uri=None, hepump_type=None, hepump_io='hepump_io',
|
||||||
class HePump:
|
hepump='hepump', hepump_mot='hepump_mot', hepump_valve='hepump_valve',
|
||||||
def __init__(self, Mod, hepump_uri, hepump_io='hepump_io', hemotname='hepump_mot', **kwds):
|
flow_sensor='flow_sensor', pump_pressure='pump_pressure', nv='nv',
|
||||||
Mod(hepump_io, 'frappy_psi.trinamic.BytesIO', 'He pump connection', uri=hepump_uri)
|
ccu_io='ccu_io', **kwds):
|
||||||
Mod(hemotname, 'frappy_psi.trinamic.Motor', 'He pump valve motor', io=hepump_io)
|
Mod = self.modfactory
|
||||||
|
hepump_type = hepump_type or self.config.get('hepump_type', 'no')
|
||||||
|
Mod(nv, 'frappy_psi.ccu4.NeedleValveFlow', 'flow from flow sensor or pump pressure',
|
||||||
class Rack:
|
flow_sensor=flow_sensor, pressure=pump_pressure, io=ccu_io)
|
||||||
rackfile = '/home/l_samenv/.config/racks.ini'
|
Mod(pump_pressure, 'frappy_psi.ccu4.Pressure', 'He pump pressure', io=ccu_io)
|
||||||
|
if hepump_type == 'no':
|
||||||
def __init__(self, modfactory, **kwds):
|
print('no pump, no flow meter - using flow from pressure alone')
|
||||||
self.modfactory = modfactory
|
return
|
||||||
parser = ConfigParser()
|
hepump_uri = hepump_uri or self.config['hepump_uri']
|
||||||
parser.optionxform = str
|
Mod(hepump_io, 'frappy.io.BytesIO', 'He pump connection', uri=hepump_uri)
|
||||||
parser.read([self.rackfile])
|
Mod(hepump, 'frappy_psi.hepump.HePump', 'He pump', pump_type=hepump_type,
|
||||||
kwds.update(parser.items(os.environ['Instrument']))
|
valvemotor=hepump_mot, valve=hepump_valve, flow=nv)
|
||||||
self.kwds = kwds
|
Mod(hepump_mot, 'frappy_psi.hepump.Motor', 'He pump valve motor', io=hepump_io, maxcurrent=2.8)
|
||||||
|
Mod(hepump_valve, 'frappy_psi.butterflyvalve.Valve', 'He pump valve', motor=hepump_mot)
|
||||||
def lakeshore(self):
|
Mod(flow_sensor, 'frappy_psi.sensirion.FlowSensor', 'Flow Sensor', io=hepump_io, nsamples=160)
|
||||||
return Lsc(self.modfactory, **self.kwds)
|
|
||||||
|
|
||||||
def ccu(self, **kwds):
|
|
||||||
kwds.update(self.kwds)
|
|
||||||
return CCU(self.modfactory, **kwds)
|
|
||||||
|
|
||||||
def hepump(self):
|
|
||||||
return HePump(self.modfactory, **self.kwds)
|
|
||||||
|
|
||||||
|
@ -23,20 +23,21 @@
|
|||||||
import time
|
import time
|
||||||
import math
|
import math
|
||||||
from frappy.lib.enum import Enum
|
from frappy.lib.enum import Enum
|
||||||
|
from frappy.lib import clamp
|
||||||
# the most common Frappy classes can be imported from frappy.core
|
# the most common Frappy classes can be imported from frappy.core
|
||||||
from frappy.core import HasIO, Parameter, Command, Readable, Writable, Drivable, \
|
from frappy.core import HasIO, Parameter, Command, Readable, Writable, Drivable, \
|
||||||
Property, StringIO, BUSY, IDLE, WARN, ERROR, DISABLED, Attached
|
Property, StringIO, BUSY, IDLE, WARN, ERROR, DISABLED, Attached, nopoll
|
||||||
from frappy.datatypes import BoolType, EnumType, FloatRange, StructOf, \
|
from frappy.datatypes import BoolType, EnumType, FloatRange, StructOf, \
|
||||||
StatusType, IntRange, StringType, TupleOf
|
StatusType, IntRange, StringType, TupleOf
|
||||||
from frappy.errors import CommunicationFailedError
|
from frappy.errors import CommunicationFailedError
|
||||||
from frappy.states import HasStates, status_code, Retry
|
from frappy.states import HasStates, status_code, Retry
|
||||||
|
|
||||||
|
|
||||||
M = Enum(idle=0, opening=1, closing=2, opened=3, closed=5, no_motor=6)
|
M = Enum(idle=0, opening=1, closing=2, opened=3, closed=4, no_motor=5)
|
||||||
A = Enum(disabled=0, manual=1, auto=2)
|
A = Enum(disabled=0, manual=1, auto=2)
|
||||||
|
|
||||||
|
|
||||||
class CCU4IO(StringIO):
|
class IO(StringIO):
|
||||||
"""communication with CCU4"""
|
"""communication with CCU4"""
|
||||||
# for completeness: (not needed, as it is the default)
|
# for completeness: (not needed, as it is the default)
|
||||||
end_of_line = '\n'
|
end_of_line = '\n'
|
||||||
@ -44,8 +45,8 @@ class CCU4IO(StringIO):
|
|||||||
identification = [('cid', r'cid=CCU4.*')]
|
identification = [('cid', r'cid=CCU4.*')]
|
||||||
|
|
||||||
|
|
||||||
class CCU4Base(HasIO):
|
class Base(HasIO):
|
||||||
ioClass = CCU4IO
|
ioClass = IO
|
||||||
|
|
||||||
def command(self, **kwds):
|
def command(self, **kwds):
|
||||||
"""send a command and get the response
|
"""send a command and get the response
|
||||||
@ -79,7 +80,7 @@ class CCU4Base(HasIO):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
class HeLevel(CCU4Base, Readable):
|
class HeLevel(Base, Readable):
|
||||||
"""He Level channel of CCU4"""
|
"""He Level channel of CCU4"""
|
||||||
|
|
||||||
value = Parameter(unit='%')
|
value = Parameter(unit='%')
|
||||||
@ -121,10 +122,10 @@ class HeLevel(CCU4Base, Readable):
|
|||||||
return self.command(hfu=value)
|
return self.command(hfu=value)
|
||||||
|
|
||||||
|
|
||||||
class Valve(CCU4Base, Writable):
|
class Valve(Base, Writable):
|
||||||
value = Parameter('relay state', BoolType())
|
value = Parameter('relay state', BoolType())
|
||||||
target = Parameter('relay target', BoolType())
|
target = Parameter('relay target', BoolType())
|
||||||
ioClass = CCU4IO
|
ioClass = IO
|
||||||
STATE_MAP = {0: (0, (IDLE, 'off')),
|
STATE_MAP = {0: (0, (IDLE, 'off')),
|
||||||
1: (1, (IDLE, 'on')),
|
1: (1, (IDLE, 'on')),
|
||||||
2: (0, (ERROR, 'no valve')),
|
2: (0, (ERROR, 'no valve')),
|
||||||
@ -173,7 +174,7 @@ class N2TempSensor(Readable):
|
|||||||
value = Parameter('LN2 T sensor', FloatRange(unit='K'), default=0)
|
value = Parameter('LN2 T sensor', FloatRange(unit='K'), default=0)
|
||||||
|
|
||||||
|
|
||||||
class N2Level(CCU4Base, Readable):
|
class N2Level(Base, Readable):
|
||||||
valve = Attached(Writable, mandatory=False)
|
valve = Attached(Writable, mandatory=False)
|
||||||
lower = Attached(Readable, mandatory=False)
|
lower = Attached(Readable, mandatory=False)
|
||||||
upper = Attached(Readable, mandatory=False)
|
upper = Attached(Readable, mandatory=False)
|
||||||
@ -285,138 +286,320 @@ class N2Level(CCU4Base, Readable):
|
|||||||
self.command(nc=0)
|
self.command(nc=0)
|
||||||
|
|
||||||
|
|
||||||
class FlowPressure(CCU4Base, Readable):
|
class HasFilter:
|
||||||
|
__value1 = None
|
||||||
|
__value = None
|
||||||
|
__last = None
|
||||||
|
|
||||||
|
def filter(self, filter_time, value):
|
||||||
|
now = time.time()
|
||||||
|
if self.__value is None:
|
||||||
|
self.__last = now
|
||||||
|
self.__value1 = value
|
||||||
|
self.__value = value
|
||||||
|
weight = (now - self.__last) / filter_time
|
||||||
|
self.__value1 += weight * (value - self.__value)
|
||||||
|
self.__value += weight * (self.__value1 - self.__value)
|
||||||
|
self.__last = now
|
||||||
|
return self.__value
|
||||||
|
|
||||||
|
|
||||||
|
class Pressure(HasFilter, Base, Readable):
|
||||||
value = Parameter(unit='mbar')
|
value = Parameter(unit='mbar')
|
||||||
mbar_offset = Parameter(unit='mbar', default=0.8, readonly=False)
|
mbar_offset = Parameter('offset in mbar', FloatRange(unit='mbar'), default=0.8, readonly=False)
|
||||||
|
filter_time = Parameter('filter time', FloatRange(unit='sec'), readonly=False, default=3)
|
||||||
pollinterval = Parameter(default=0.25)
|
pollinterval = Parameter(default=0.25)
|
||||||
|
|
||||||
def read_value(self):
|
def read_value(self):
|
||||||
return self.filter(self.command(f=float)) - self.mbar_offset
|
return self.filter(self.filter_time, self.command(f=float)) - self.mbar_offset
|
||||||
|
|
||||||
|
|
||||||
class NeedleValve(HasStates, CCU4Base, Drivable):
|
class NeedleValveFlow(HasStates, Base, Drivable):
|
||||||
flow = Attached(Readable, mandatory=False)
|
flow_sensor = Attached(Readable, mandatory=False)
|
||||||
flow_pressure = Attached(Readable, mandatory=False)
|
pressure = Attached(Pressure, mandatory=False)
|
||||||
|
use_pressure = Parameter('flag (use pressure instead of flow meter)', BoolType(),
|
||||||
|
readonly=False, default=False)
|
||||||
|
lnm_per_mbar = Parameter('scale factor', FloatRange(unit='lnm/mbar'), readonly=False, default=0.6)
|
||||||
|
|
||||||
value = Parameter(unit='ln/min')
|
value = Parameter(unit='ln/min')
|
||||||
target = Parameter(unit='ln/min')
|
target = Parameter(unit='ln/min')
|
||||||
|
|
||||||
lnm_per_mbar = Parameter(unit='ln/min/mbar', default=0.6, readonly=False)
|
motor_state = Parameter('motor_state', EnumType(M), default=0)
|
||||||
use_pressure = Parameter('use flow from pressure', BoolType(),
|
|
||||||
default=False, readonly=False)
|
|
||||||
motor_state = Parameter('motor_state', EnumType(M))
|
|
||||||
tolerance = Parameter('tolerance', FloatRange(0), value=0.25, readonly=False)
|
tolerance = Parameter('tolerance', FloatRange(0), value=0.25, readonly=False)
|
||||||
tolerance2 = Parameter('tolerance limit above 2 lnm', FloatRange(0), value=0.5, readonly=False)
|
tolerance2 = Parameter('tolerance limit above 2 lnm', FloatRange(0), value=0.5, readonly=False)
|
||||||
prop = Parameter('proportional term', FloatRange(unit='s/lnm'), readonly=False)
|
prop = Parameter('proportional term', FloatRange(unit='s/lnm'), readonly=False, default=0.001)
|
||||||
deriv = Parameter('min progress time constant', FloatRange(unit='s'),
|
deriv = Parameter('min progress time constant', FloatRange(unit='s'),
|
||||||
default=30, readonly=False)
|
default=30, readonly=False)
|
||||||
settle = Parameter('time within tolerance before getting quiet', FloatRange(unit='s'),
|
settle = Parameter('time within tolerance before getting quiet', FloatRange(unit='s'),
|
||||||
default=30, readonly=False)
|
default=30, readonly=False)
|
||||||
step_factor = Parameter('factor (no progress time) / (min step size)', FloatRange(), default=300)
|
step_factor = Parameter('factor (no progress time) / (min step size)', FloatRange(), default=300, readonly=False)
|
||||||
control_active = Parameter('control active flag', BoolType(), readonly=False)
|
control_active = Parameter('control active flag', BoolType(), readonly=False, default=1)
|
||||||
pollinterval = Parameter(default=1)
|
min_open_step = Parameter('minimal open step', FloatRange(unit='s'), readonly=False, default=0.06)
|
||||||
|
min_close_step = Parameter('minimal close step', FloatRange(unit='s'), readonly=False, default=0.05)
|
||||||
|
raw_open_step = Parameter('step after direction change', FloatRange(unit='s'), readonly=False, default=0.12)
|
||||||
|
raw_close_step = Parameter('step after direction change', FloatRange(unit='s'), readonly=False, default=0.04)
|
||||||
|
pollinterval = Parameter(default=5)
|
||||||
_ref_time = 0
|
_ref_time = 0
|
||||||
_ref_dif = 0
|
_ref_dif = 0
|
||||||
_last_cycle = 0
|
_dir = 0
|
||||||
_last_progress = 0
|
_rawdir = 0
|
||||||
_step = 0
|
_step = 0
|
||||||
|
|
||||||
def initModule(self):
|
def initModule(self):
|
||||||
|
if self.pressure:
|
||||||
|
self.pressure.addCallback('value', self.update_from_pressure)
|
||||||
|
if self.flow_sensor:
|
||||||
|
self.flow_sensor.addCallback('value', self.update_from_flow)
|
||||||
super().initModule()
|
super().initModule()
|
||||||
if self.flow_pressure:
|
|
||||||
self.flow_pressure.addCallback('value', self.update_flow_pressure)
|
def update_from_flow(self, value):
|
||||||
if self.flow:
|
if not self.use_pressure:
|
||||||
self.flow.addCallback('value', self.update_flow)
|
self.value = value
|
||||||
self.write_tolerance(self.tolerance)
|
# self.cycle_machine()
|
||||||
|
|
||||||
|
def update_from_pressure(self, value):
|
||||||
|
if self.use_pressure:
|
||||||
|
self.value = value * self.lnm_per_mbar
|
||||||
|
# self.cycle_machine()
|
||||||
|
|
||||||
|
# def doPoll(self):
|
||||||
|
# """only the updates should trigger the machine"""
|
||||||
|
|
||||||
|
def read_value(self):
|
||||||
|
p = self.pressure.read_value() * self.lnm_per_mbar
|
||||||
|
f = self.flow_sensor.read_value()
|
||||||
|
# self.log.info('p %g f %g +- %.2g', p, f, self.flow_sensor.stddev)
|
||||||
|
self.read_motor_state()
|
||||||
|
return p if self.use_pressure else f
|
||||||
|
|
||||||
def write_tolerance(self, tolerance):
|
def write_tolerance(self, tolerance):
|
||||||
if hasattr(self.flow_pressure, 'tolerance'):
|
if hasattr(self.pressure, 'tolerance'):
|
||||||
self.flow_pressure.tolerance = tolerance / self.lnm_per_mbar
|
self.pressure.tolerance = tolerance / self.lnm_per_mbar
|
||||||
if hasattr(self.flow, 'tolerance'):
|
if hasattr(self.flow_sensor, 'tolerance'):
|
||||||
self.flow.tolerance = tolerance
|
self.flow_sensor.tolerance = tolerance
|
||||||
|
|
||||||
def read_use_pressure(self):
|
def read_use_pressure(self):
|
||||||
if self.flow_pressure:
|
if self.pressure:
|
||||||
if self.flow:
|
if self.flow_sensor:
|
||||||
return self.use_pressure
|
return self.use_pressure
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def update_flow(self, value):
|
|
||||||
if not self.use_pressure:
|
|
||||||
self.value = value
|
|
||||||
self.cycle_machine()
|
|
||||||
|
|
||||||
def update_flow_pressure(self, value):
|
|
||||||
if self.use_pressure:
|
|
||||||
self.value = value * self.lnm_per_mbar
|
|
||||||
self.cycle_machine()
|
|
||||||
|
|
||||||
def write_target(self, value):
|
def write_target(self, value):
|
||||||
self.start_machine(self.controlling, in_tol_time=0,
|
self.start_machine(self.change_target, fast_poll=1)
|
||||||
ref_time=0, ref_dif=0, prev_dif=0)
|
|
||||||
|
|
||||||
@status_code(BUSY)
|
@status_code(BUSY)
|
||||||
def unblock_from_open(self, state):
|
def change_target(self, state):
|
||||||
self.motor_state = self.command(fm=int)
|
state.in_tol_time = 0
|
||||||
if self.motor_state == 'opened':
|
state.last_minstep = {}
|
||||||
self.command(mp=-60)
|
state.last_progress = state.now
|
||||||
return Retry
|
state.ref_time = 0
|
||||||
if self.motor_state == 'closing':
|
state.ref_dif = 0
|
||||||
return Retry
|
state.prev_dif = 0 # used?
|
||||||
if self.motor_state == 'closed':
|
state.last_close_time = 0
|
||||||
if self.value > max(1, self.target):
|
state.last_pulse_time = 0
|
||||||
return Retry
|
state.raw_fact = 1
|
||||||
state.flow_before = self.value
|
state.raw_step = 0
|
||||||
state.wiggle = 1
|
if abs(self.target - self.value) < self._tolerance():
|
||||||
state.start_wiggle = state.now
|
return self.at_target
|
||||||
self.command(mp=60)
|
return self.raw_control
|
||||||
return self.unblock_open
|
|
||||||
return self.approaching
|
def start_direction(self, state):
|
||||||
|
if self.target > self.value:
|
||||||
|
self._dir = 1
|
||||||
|
state.minstep = self.min_open_step
|
||||||
|
else:
|
||||||
|
self._dir = -1
|
||||||
|
state.minstep = self.min_close_step
|
||||||
|
state.prev = []
|
||||||
|
|
||||||
|
def perform_pulse(self, state):
|
||||||
|
tol = self._tolerance()
|
||||||
|
dif = self.target - self.value
|
||||||
|
difdir = dif * self._dir
|
||||||
|
state.last_pulse_time = state.now
|
||||||
|
if difdir > tol:
|
||||||
|
step = state.minstep + (difdir - tol) * self.prop
|
||||||
|
elif difdir > 0:
|
||||||
|
step = state.minstep * difdir / tol
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
self.log.info('MP %g dif=%g tol=%g', step * self._dir, dif, tol)
|
||||||
|
self.command(mp=clamp(-1, step * self._dir, 1))
|
||||||
|
|
||||||
@status_code(BUSY)
|
@status_code(BUSY)
|
||||||
def unblock_open(self, state):
|
def raw_control(self, state):
|
||||||
self.motor_state = self.command(fm=int)
|
tol = self._tolerance()
|
||||||
if self.value < state.flow_before:
|
if state.init:
|
||||||
state.flow_before_open = self.value
|
self.start_direction(state)
|
||||||
elif self.value > state.flow_before + 1:
|
state.raw_step = self.raw_open_step if self._dir > 0 else -self.raw_close_step
|
||||||
state.wiggle = -state.wiggle / 2
|
state.raw_fact = 1
|
||||||
self.command(mp=state.wiggle)
|
# if self.read_motor_state() == M.closed:
|
||||||
state.start_wiggle = state.now
|
# # TODO: also check for flow near lower limit ? but only once after change_target
|
||||||
return self.unblock_close
|
# self.log.info('start with fast opening')
|
||||||
if self.motor_state == 'opening':
|
# state.raw_step = 1
|
||||||
|
# self._dir = 1
|
||||||
|
difdir = (self.target - self.value) * self._dir
|
||||||
|
state.prev.append(difdir)
|
||||||
|
state.diflim = max(0, difdir - tol * 1)
|
||||||
|
state.success = 0
|
||||||
|
self.command(mp=state.raw_step)
|
||||||
|
self.log.info('first rawstep %g', state.raw_step)
|
||||||
|
state.last_pulse_time = state.now
|
||||||
|
state.raw_pulse_cnt = 0
|
||||||
|
state.cycle_cnt = 0
|
||||||
return Retry
|
return Retry
|
||||||
if self.motor_state == 'idle':
|
difdir = (self.target - self.value) * self._dir
|
||||||
self.command(mp=state.wiggle)
|
state.cycle_cnt += 1
|
||||||
|
state.prev.append(difdir)
|
||||||
|
del state.prev[:-5]
|
||||||
|
if state.prev[-1] > max(state.prev[:-1]):
|
||||||
|
# TODO: use the amount of overshoot to reduce the raw_step
|
||||||
|
state.cycle_cnt = 0
|
||||||
|
self.log.info('difference is increasing %s', ' '.join(f'{v:g}' for v in state.prev))
|
||||||
return Retry
|
return Retry
|
||||||
if self.motor_state == 'opened':
|
if state.cycle_cnt >= 5:
|
||||||
if state.now < state.start_wiggle + 20:
|
state.cycle_cnt = 0
|
||||||
return Retry
|
state.diflim = max(tol, min(state.prev) - tol * 0.5)
|
||||||
return self.final_status(ERROR, 'can not open')
|
state.raw_pulse_cnt += 1
|
||||||
|
self.command(mp=state.raw_step * state.raw_fact)
|
||||||
|
self.log.info('rawstep %g', state.raw_step)
|
||||||
|
if state.raw_pulse_cnt % 5 == 0 and state.raw_pulse_cnt > 5:
|
||||||
|
state.raw_fact *= 1.25
|
||||||
|
return Retry
|
||||||
|
if difdir >= state.diflim:
|
||||||
|
state.success = max(0, state.success - 1)
|
||||||
|
return Retry
|
||||||
|
state.success += 1
|
||||||
|
if state.success <= 3:
|
||||||
|
return Retry
|
||||||
|
if state.raw_pulse_cnt < 3:
|
||||||
|
state.raw_fact = 1 - (3 - state.raw_pulse_cnt) ** 2 * 0.05
|
||||||
|
if state.raw_fact != 1:
|
||||||
|
if self._dir > 0:
|
||||||
|
self.raw_open_step *= state.raw_fact
|
||||||
|
self.log.info('raw_open_step %g f=%g', self.raw_open_step, state.raw_fact)
|
||||||
|
self.min_open_pulse = min(self.min_open_pulse, self.raw_open_step)
|
||||||
|
else:
|
||||||
|
self.raw_close_step *= state.raw_fact
|
||||||
|
self.log.info('raw_close_step %g f=%g', self.raw_close_step, state.raw_fact)
|
||||||
|
self.min_close_pulse = min(self.min_close_pulse, self.raw_close_step)
|
||||||
return self.controlling
|
return self.controlling
|
||||||
|
|
||||||
|
# @status_code(BUSY)
|
||||||
|
# def raw_control(self, state):
|
||||||
|
# tol = self._tolerance()
|
||||||
|
# if state.init:
|
||||||
|
# self.start_direction(state)
|
||||||
|
# if self._dir != self._rawdir:
|
||||||
|
# self._rawdir = self._dir
|
||||||
|
# state.first_step = self.first_open_step if self._dir > 0 else -self.first_close_step
|
||||||
|
# else:
|
||||||
|
# state.first_step = 0
|
||||||
|
# state.first_fact = 1
|
||||||
|
# # if self.read_motor_state() == M.closed:
|
||||||
|
# # # TODO: also check for flow near lower limit ? but only once after change_target
|
||||||
|
# # self.log.info('start with fast opening')
|
||||||
|
# # state.first_step = 1
|
||||||
|
# # self._dir = 1
|
||||||
|
# difdir = (self.target - self.value) * self._dir
|
||||||
|
# state.prev = [difdir]
|
||||||
|
# state.diflim = max(0, difdir - tol * 0.5)
|
||||||
|
# state.success = 0
|
||||||
|
# if state.first_step:
|
||||||
|
# self.command(mp=state.first_step)
|
||||||
|
# else:
|
||||||
|
# self.perform_pulse(state)
|
||||||
|
# self.log.info('firststep %g', state.first_step)
|
||||||
|
# state.last_pulse_time = state.now
|
||||||
|
# state.raw_pulse_cnt = 0
|
||||||
|
# return Retry
|
||||||
|
# difdir = (self.target - self.value) * self._dir
|
||||||
|
# if state.delta(5):
|
||||||
|
# state.diflim = max(0, min(state.prev) - tol * 0.1)
|
||||||
|
# state.prev = [difdir]
|
||||||
|
# state.raw_pulse_cnt += 1
|
||||||
|
# if state.first_step and state.raw_pulse_cnt % 10 == 0:
|
||||||
|
# self.command(mp=state.first_step * state.first_fact)
|
||||||
|
# self.log.info('repeat firststep %g', state.first_step * state.first_fact)
|
||||||
|
# state.first_fact *= 1.25
|
||||||
|
# else:
|
||||||
|
# self.perform_pulse(state)
|
||||||
|
# return Retry
|
||||||
|
# state.prev.append(difdir)
|
||||||
|
# if difdir >= state.diflim:
|
||||||
|
# state.success = max(0, state.success - 1)
|
||||||
|
# return Retry
|
||||||
|
# state.success += 1
|
||||||
|
# if state.success <= 5:
|
||||||
|
# return Retry
|
||||||
|
# if state.first_step:
|
||||||
|
# if state.raw_pulse_cnt < 3:
|
||||||
|
# state.first_fact = 1 - (3 - state.raw_pulse_cnt) ** 2 * 0.04
|
||||||
|
# if state.first_fact != 1:
|
||||||
|
# if self._dir > 0:
|
||||||
|
# self.first_open_step *= state.first_fact
|
||||||
|
# self.log.info('first_open_step %g f=%g', self.first_open_step, state.first_fact)
|
||||||
|
# else:
|
||||||
|
# self.first_close_step *= state.first_fact
|
||||||
|
# self.log.info('first_close_step %g f=%g', self.first_close_step, state.first_fact)
|
||||||
|
# return self.controlling
|
||||||
|
|
||||||
@status_code(BUSY)
|
@status_code(BUSY)
|
||||||
def unblock_close(self, state):
|
def controlling(self, state):
|
||||||
self.motor_state = self.command(fm=int)
|
dif = self.target - self.value
|
||||||
if self.value > state.flow_before:
|
if state.init:
|
||||||
state.flow_before_open = self.value
|
self.start_direction(state)
|
||||||
elif self.value < state.flow_before - 1:
|
state.ref_dif = abs(dif)
|
||||||
if state.wiggle < self.prop * 2:
|
state.ref_time = state.now
|
||||||
return self.final_status(IDLE, '')
|
state.in_tol_time = 0
|
||||||
state.wiggle = -state.wiggle / 2
|
difdir = dif * self._dir # negative when overshoot happend
|
||||||
self.command(mp=state.wiggle)
|
# difdif = dif - state.prev_dif
|
||||||
state.start_wiggle = state.now
|
# state.prev_dif = dif
|
||||||
return self.unblock_open
|
expected_dif = state.ref_dif * math.exp((state.ref_time - state.now) / self.deriv)
|
||||||
if self.motor_state == 'closing':
|
|
||||||
|
tol = self._tolerance()
|
||||||
|
if difdir < tol:
|
||||||
|
# prev_minstep = state.last_minstep.pop(self._dir, None)
|
||||||
|
# attr = 'min_open_step' if self._dir > 0 else 'min_close_step'
|
||||||
|
# if prev_minstep is not None:
|
||||||
|
# # increase minstep
|
||||||
|
# minstep = getattr(self, attr)
|
||||||
|
# setattr(self, attr, minstep * 1.1)
|
||||||
|
# self.log.info('increase %s to %g', attr, minstep)
|
||||||
|
if difdir > -tol: # within tolerance
|
||||||
|
delta = state.delta()
|
||||||
|
state.in_tol_time += delta
|
||||||
|
if state.in_tol_time > self.settle:
|
||||||
|
# state.last_minstep.pop(self._dir, None)
|
||||||
|
self.log.info('at target %g %g', dif, tol)
|
||||||
|
return self.at_target
|
||||||
|
if difdir < 0:
|
||||||
|
return Retry
|
||||||
|
# self.log.info('minstep=0 dif=%g', dif)
|
||||||
|
else: # overshoot
|
||||||
|
self.log.info('overshoot %g', dif)
|
||||||
|
return self.raw_control
|
||||||
|
# # overshoot
|
||||||
|
# prev_minstep = state.last_minstep.pop(self._dir, None)
|
||||||
|
# if prev_minstep is None:
|
||||||
|
# minstep = getattr(self, attr) * 0.9
|
||||||
|
# self.log.info('decrease %s to %g', attr, minstep)
|
||||||
|
# setattr(self, attr, minstep)
|
||||||
|
# self.start_step(state, self.target)
|
||||||
|
# still approaching
|
||||||
|
if difdir <= expected_dif:
|
||||||
|
if difdir < expected_dif / 1.25 - tol:
|
||||||
|
state.ref_time = state.now
|
||||||
|
state.ref_dif = (difdir + tol) * 1.25
|
||||||
|
# self.log.info('new ref %g', state.ref_dif)
|
||||||
|
state.last_progress = state.now
|
||||||
|
return Retry # progressing: no pulse needed
|
||||||
|
if state.now < state.last_pulse_time + 2.5:
|
||||||
return Retry
|
return Retry
|
||||||
if self.motor_state == 'idle':
|
# TODO: check motor state for closed / opened ?
|
||||||
self.command(mp=state.wiggle)
|
self.perform_pulse(state)
|
||||||
return Retry
|
return Retry
|
||||||
if self.motor_state == 'closed':
|
|
||||||
if state.now < state.start_wiggle + 20:
|
|
||||||
return Retry
|
|
||||||
return self.final_status(ERROR, 'can not close')
|
|
||||||
return self.final_status(WARN, 'unblock interrupted')
|
|
||||||
|
|
||||||
def _tolerance(self):
|
def _tolerance(self):
|
||||||
return min(self.tolerance * min(1, self.value / 2), self.tolerance2)
|
return min(self.tolerance * min(1, self.value / 2), self.tolerance2)
|
||||||
@ -426,48 +609,67 @@ class NeedleValve(HasStates, CCU4Base, Drivable):
|
|||||||
dif = self.target - self.value
|
dif = self.target - self.value
|
||||||
if abs(dif) > self._tolerance():
|
if abs(dif) > self._tolerance():
|
||||||
state.in_tol_time = 0
|
state.in_tol_time = 0
|
||||||
|
self.log.info('unstable %g', dif)
|
||||||
return self.unstable
|
return self.unstable
|
||||||
return Retry
|
return Retry
|
||||||
|
|
||||||
@status_code(IDLE, 'unstable')
|
@status_code(IDLE, 'unstable')
|
||||||
def unstable(self, state):
|
def unstable(self, state):
|
||||||
|
difdir = (self.target - self.value) * self._dir
|
||||||
|
if difdir < 0 or self._dir == 0:
|
||||||
|
return self.raw_control
|
||||||
return self.controlling(state)
|
return self.controlling(state)
|
||||||
|
|
||||||
@status_code(BUSY)
|
def read_motor_state(self):
|
||||||
def controlling(self, state):
|
return self.command(fm=int)
|
||||||
delta = state.delta(0)
|
|
||||||
dif = self.target - self.value
|
|
||||||
difdif = dif - state.prev_dif
|
|
||||||
state.prev_dif = dif
|
|
||||||
self.motor_state = self.command(fm=int)
|
|
||||||
if self.motor_state == 'closed':
|
|
||||||
if dif < 0 or difdif < 0:
|
|
||||||
return Retry
|
|
||||||
return self.unblock_from_open
|
|
||||||
elif self.motor_state == 'opened': # trigger also when flow too high?
|
|
||||||
if dif > 0 or difdif > 0:
|
|
||||||
return Retry
|
|
||||||
self.command(mp=-60)
|
|
||||||
return self.unblock_from_open
|
|
||||||
|
|
||||||
tolerance = self._tolerance()
|
@Command
|
||||||
if abs(dif) < tolerance:
|
def close(self):
|
||||||
state.in_tol_time += delta
|
"""close valve fully"""
|
||||||
if state.in_tol_time > self.settle:
|
self.command(mp=-60)
|
||||||
return self.at_target
|
self.motor_state = self.command(fm=int)
|
||||||
|
self.start_machine(self.closing)
|
||||||
|
|
||||||
|
@status_code(BUSY)
|
||||||
|
def closing(self, state):
|
||||||
|
if state.init:
|
||||||
|
state.start_time = state.now
|
||||||
|
self.read_motor_state()
|
||||||
|
if self.motor_state == M.closing:
|
||||||
return Retry
|
return Retry
|
||||||
expected_dif = state.ref_dif * math.exp((state.now - state.ref_time) / self.deriv)
|
if self.motor_state == M.closed:
|
||||||
if abs(dif) < expected_dif:
|
return self.final_status(IDLE, 'closed')
|
||||||
if abs(dif) < expected_dif / 1.25:
|
if state.now < state.start_time + 1:
|
||||||
state.ref_time = state.now
|
|
||||||
state.ref_dif = abs(dif) * 1.25
|
|
||||||
state.last_progress = state.now
|
|
||||||
return Retry # progress is fast enough
|
|
||||||
state.ref_time = state.now
|
|
||||||
state.ref_dif = abs(dif)
|
|
||||||
state.step += dif * delta * self.prop
|
|
||||||
if abs(state.step) < (state.now - state.last_progress) / self.step_factor:
|
|
||||||
# wait until step size is big enough
|
|
||||||
return Retry
|
return Retry
|
||||||
self.command(mp=state.step)
|
return self.final_status(IDLE, 'fixed')
|
||||||
return Retry
|
|
||||||
|
@Command
|
||||||
|
def open(self):
|
||||||
|
"""open valve fully"""
|
||||||
|
self.command(mp=60)
|
||||||
|
self.read_motor_state()
|
||||||
|
self.start_machine(self.opening)
|
||||||
|
|
||||||
|
@status_code(BUSY)
|
||||||
|
def opening(self, state):
|
||||||
|
if state.init:
|
||||||
|
state.start_time = state.now
|
||||||
|
self.read_motor_state()
|
||||||
|
if self.motor_state == M.opening:
|
||||||
|
return Retry
|
||||||
|
if self.motor_state == M.opened:
|
||||||
|
return self.final_status(IDLE, 'opened')
|
||||||
|
if state.now < state.start_time + 1:
|
||||||
|
return Retry
|
||||||
|
return self.final_status(IDLE, 'fixed')
|
||||||
|
|
||||||
|
@Command(FloatRange())
|
||||||
|
def pulse(self, value):
|
||||||
|
"""perform a motor pulse"""
|
||||||
|
self.log.info('pulse %g', value)
|
||||||
|
self.command(mp=value)
|
||||||
|
if value > 0:
|
||||||
|
self.motor_state = M.opening
|
||||||
|
return self.opening
|
||||||
|
self.motor_state = M.closing
|
||||||
|
return self.closing
|
||||||
|
71
frappy_psi/hepump.py
Normal file
71
frappy_psi/hepump.py
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
# *****************************************************************************
|
||||||
|
#
|
||||||
|
# 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>
|
||||||
|
#
|
||||||
|
# *****************************************************************************
|
||||||
|
|
||||||
|
from frappy.core import BoolType, FloatRange, Parameter, Readable, Writable, Attached, EnumType, nopoll
|
||||||
|
from frappy_psi.trinamic import Motor
|
||||||
|
from frappy_psi.ccu4 import Pressure, NeedleValveFlow
|
||||||
|
|
||||||
|
|
||||||
|
class ValveMotor(Motor):
|
||||||
|
has_inputs = True
|
||||||
|
|
||||||
|
|
||||||
|
class HePump(Writable):
|
||||||
|
valvemotor = Attached(Motor)
|
||||||
|
flow = Attached(NeedleValveFlow)
|
||||||
|
valve = Attached(Writable)
|
||||||
|
value = Parameter(datatype=BoolType())
|
||||||
|
target = Parameter(datatype=BoolType())
|
||||||
|
pump_type = Parameter('pump type', EnumType(no=0, neodry=1, xds35=2, sv65=3), readonly=False, default=0)
|
||||||
|
eco_mode = Parameter('eco mode', BoolType(), readonly=False)
|
||||||
|
has_feedback = Parameter('feedback works', BoolType(), readonly=False, default=True)
|
||||||
|
|
||||||
|
FLOW_SCALE = {'no': 0, 'neodry': 0.55, 'xds35': 0.6, 'sv65': 0.9}
|
||||||
|
|
||||||
|
def write_target(self, value):
|
||||||
|
self.valvemotor.write_output0(value)
|
||||||
|
|
||||||
|
def read_target(self):
|
||||||
|
return self.valvemotor.read_output0()
|
||||||
|
|
||||||
|
def read_value(self):
|
||||||
|
if self.has_feedback:
|
||||||
|
return not self.valvemotor.read_input3()
|
||||||
|
return self.target
|
||||||
|
|
||||||
|
def write_pump_type(self, value):
|
||||||
|
self.flow.pressure_scale = self.FLOW_SCALE[value.name]
|
||||||
|
|
||||||
|
def read_eco_mode(self):
|
||||||
|
if self.pump_type == 'xds35':
|
||||||
|
return self.valvemotor.read_output1()
|
||||||
|
return False
|
||||||
|
|
||||||
|
def write_eco_mode(self, value):
|
||||||
|
if self.pump_type == 'xds35':
|
||||||
|
return self.valvemotor.write_output1(value)
|
||||||
|
# else silently ignore
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -39,6 +39,13 @@ from frappy.extparams import StructParam
|
|||||||
from frappy_psi.calcurve import CalCurve
|
from frappy_psi.calcurve import CalCurve
|
||||||
|
|
||||||
|
|
||||||
|
def string_to_num(string):
|
||||||
|
try:
|
||||||
|
return int(string)
|
||||||
|
except ValueError:
|
||||||
|
return float(string)
|
||||||
|
|
||||||
|
|
||||||
class IO(StringIO):
|
class IO(StringIO):
|
||||||
"""IO classes of most LakeShore models will inherit from this"""
|
"""IO classes of most LakeShore models will inherit from this"""
|
||||||
end_of_line = '\n'
|
end_of_line = '\n'
|
||||||
@ -97,8 +104,9 @@ class HasLscIO(HasIO):
|
|||||||
if not args:
|
if not args:
|
||||||
self.communicate(f'{msghead};*OPC?')
|
self.communicate(f'{msghead};*OPC?')
|
||||||
return None
|
return None
|
||||||
converters = [type(a) for a in args]
|
# when an argument is given as integer, it might be that this argument might be a float
|
||||||
values = [a if issubclass(c, str) else f'{a:g}'
|
converters = [string_to_num if isinstance(a, int) else type(a) for a in args]
|
||||||
|
values = [a if isinstance(a, str) else f'{a:g}'
|
||||||
for c, a in zip(converters, args)]
|
for c, a in zip(converters, args)]
|
||||||
if ' ' in msghead:
|
if ' ' in msghead:
|
||||||
query = msghead.replace(' ', '? ', 1)
|
query = msghead.replace(' ', '? ', 1)
|
||||||
@ -839,7 +847,7 @@ class MainOutput(Output):
|
|||||||
}
|
}
|
||||||
|
|
||||||
_htr_range = 1
|
_htr_range = 1
|
||||||
heater_ranges = {5 - i: 10 ** - i for i in range(5)}
|
heater_ranges = {5 - i: 10 ** -i for i in range(5)}
|
||||||
sorted_factors = sorted((v, i) for i, v in heater_ranges.items())
|
sorted_factors = sorted((v, i) for i, v in heater_ranges.items())
|
||||||
|
|
||||||
def read_status(self):
|
def read_status(self):
|
||||||
@ -847,6 +855,9 @@ class MainOutput(Output):
|
|||||||
return self.HTRST_MAP[st]
|
return self.HTRST_MAP[st]
|
||||||
|
|
||||||
def configure(self):
|
def configure(self):
|
||||||
|
if self._desired_max_power is None:
|
||||||
|
self.log.info(f'max_heater {self.writeDict} {self.max_heater}')
|
||||||
|
self.write_max_heater(self.max_heater)
|
||||||
self._htr_range = irng = self.get_best_power_idx(self._desired_max_power)
|
self._htr_range = irng = self.get_best_power_idx(self._desired_max_power)
|
||||||
user_current = max(0.1, min(self.imax, 2 * math.sqrt(self._desired_max_power /
|
user_current = max(0.1, min(self.imax, 2 * math.sqrt(self._desired_max_power /
|
||||||
self.heater_ranges[irng] / self.resistance)))
|
self.heater_ranges[irng] / self.resistance)))
|
||||||
@ -1180,7 +1191,7 @@ class MainOutput336(MainOutput):
|
|||||||
imax = 2
|
imax = 2
|
||||||
vmax = 50
|
vmax = 50
|
||||||
# 3 ranges only
|
# 3 ranges only
|
||||||
heater_ranges = {i: 10 ** (3 - i) for i in range(5)}
|
heater_ranges = {3 - i: 10 ** -i for i in range(3)}
|
||||||
sorted_factors = sorted((v, i) for i, v in heater_ranges.items())
|
sorted_factors = sorted((v, i) for i, v in heater_ranges.items())
|
||||||
|
|
||||||
|
|
||||||
|
96
frappy_psi/sensirion.py
Normal file
96
frappy_psi/sensirion.py
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
# *****************************************************************************
|
||||||
|
#
|
||||||
|
# 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>
|
||||||
|
#
|
||||||
|
# *****************************************************************************
|
||||||
|
|
||||||
|
"""sensirion flow sensor,
|
||||||
|
|
||||||
|
onnected via an Arduio Nano on a serial connection
|
||||||
|
shared with the trinamic hepump valve motor
|
||||||
|
"""
|
||||||
|
|
||||||
|
import math
|
||||||
|
from frappy.core import Parameter, Readable, IntRange, FloatRange, BoolType, BytesIO, HasIO
|
||||||
|
from frappy.errors import ProgrammingError
|
||||||
|
|
||||||
|
|
||||||
|
class FlowSensor(HasIO, Readable):
|
||||||
|
value = Parameter(unit='ln/min')
|
||||||
|
stddev = Parameter('std dev.', FloatRange(unit='ln/min'))
|
||||||
|
nsamples = Parameter('number of samples for averaging', IntRange(1,1024), default=160)
|
||||||
|
offset = Parameter('offset correction', FloatRange(unit='ln/min'), readonly=False, default=0)
|
||||||
|
scale = Parameter('scale factor', FloatRange(), readonly=False, default=2.3)
|
||||||
|
saved = Parameter('is the current value saved?', BoolType(), readonly=False)
|
||||||
|
pollinterval = Parameter(default=0.2)
|
||||||
|
|
||||||
|
ioClass = BytesIO
|
||||||
|
_saved = None
|
||||||
|
|
||||||
|
def command(self, cmd, nvalues=1):
|
||||||
|
if len(cmd) == 1: # its a query
|
||||||
|
command = f'{cmd}\n'
|
||||||
|
else:
|
||||||
|
if len(cmd) > 7:
|
||||||
|
raise ProgrammingError('number does not fit into 6 characters')
|
||||||
|
command = f'{cmd[0]}{cmd[1:].ljust(6)}\n'
|
||||||
|
reply = self.io.communicate(command.encode('ascii'), max(1, nvalues * 9))
|
||||||
|
if nvalues == 1:
|
||||||
|
return float(reply)
|
||||||
|
if nvalues:
|
||||||
|
return tuple(float(s) for s in reply.split())
|
||||||
|
return None
|
||||||
|
|
||||||
|
def doPoll(self):
|
||||||
|
flow, stddev = self.command('?', nvalues=2)
|
||||||
|
stddev = stddev / math.sqrt(self.nsamples)
|
||||||
|
if (flow, stddev) != (self.value, self.stddev):
|
||||||
|
self.value, self.stddev = flow, stddev
|
||||||
|
# TODO: treat status (e.g. when reading 0 always)
|
||||||
|
|
||||||
|
def read_value(self):
|
||||||
|
self.doPoll()
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
def read_nsamples(self):
|
||||||
|
return self.command('n')
|
||||||
|
|
||||||
|
def write_nsamples(self, nsamples):
|
||||||
|
return self.command(f'n{nsamples}')
|
||||||
|
|
||||||
|
def read_offset(self):
|
||||||
|
return self.command('o')
|
||||||
|
|
||||||
|
def write_offset(self, offset):
|
||||||
|
return self.command(f'o{offset:.2f}')
|
||||||
|
|
||||||
|
def read_scale(self):
|
||||||
|
return self.command('g')
|
||||||
|
|
||||||
|
def write_scale(self, scale):
|
||||||
|
return self.command(f'g{scale:.4f}')
|
||||||
|
|
||||||
|
def read_saved(self):
|
||||||
|
if self._saved is None:
|
||||||
|
self._saved = self.read_scale(), self.read_offset()
|
||||||
|
return True
|
||||||
|
return self._saved == (self.scale, self.offset)
|
||||||
|
|
||||||
|
def write_saved(self, target):
|
||||||
|
if target:
|
||||||
|
self.command('s', nvalues=0)
|
@ -26,7 +26,8 @@ import struct
|
|||||||
|
|
||||||
from frappy.core import BoolType, Command, EnumType, FloatRange, IntRange, \
|
from frappy.core import BoolType, Command, EnumType, FloatRange, IntRange, \
|
||||||
HasIO, Parameter, Property, Drivable, PersistentMixin, PersistentParam, Done, \
|
HasIO, Parameter, Property, Drivable, PersistentMixin, PersistentParam, Done, \
|
||||||
IDLE, BUSY, ERROR, Limit
|
IDLE, BUSY, ERROR, Limit, nopoll, ArrayOf
|
||||||
|
from frappy.properties import HasProperties
|
||||||
from frappy.io import BytesIO
|
from frappy.io import BytesIO
|
||||||
from frappy.errors import CommunicationFailedError, HardwareError, RangeError, IsBusyError
|
from frappy.errors import CommunicationFailedError, HardwareError, RangeError, IsBusyError
|
||||||
from frappy.rwhandler import ReadHandler, WriteHandler
|
from frappy.rwhandler import ReadHandler, WriteHandler
|
||||||
@ -119,9 +120,6 @@ class Motor(PersistentMixin, HasIO, Drivable):
|
|||||||
move_status = Parameter('', EnumType(ok=0, stalled=1, encoder_deviation=2, stalled_and_encoder_deviation=3),
|
move_status = Parameter('', EnumType(ok=0, stalled=1, encoder_deviation=2, stalled_and_encoder_deviation=3),
|
||||||
group='hwstatus')
|
group='hwstatus')
|
||||||
error_bits = Parameter('', IntRange(0, 255), group='hwstatus')
|
error_bits = Parameter('', IntRange(0, 255), group='hwstatus')
|
||||||
home = Parameter('state of home switch (input 3)', BoolType(), group='more')
|
|
||||||
has_home = Parameter('enable home and activate pullup resistor', BoolType(),
|
|
||||||
default=True, group='more')
|
|
||||||
auto_reset = Parameter('automatic reset after failure', BoolType(), group='more', readonly=False, default=False)
|
auto_reset = Parameter('automatic reset after failure', BoolType(), group='more', readonly=False, default=False)
|
||||||
free_wheeling = writable('', FloatRange(0, 60., unit='sec', fmtstr='%.2f'),
|
free_wheeling = writable('', FloatRange(0, 60., unit='sec', fmtstr='%.2f'),
|
||||||
value=0.1, group='motorparam')
|
value=0.1, group='motorparam')
|
||||||
@ -132,6 +130,20 @@ class Motor(PersistentMixin, HasIO, Drivable):
|
|||||||
readonly=False, default=0, visibility=3, group='more')
|
readonly=False, default=0, visibility=3, group='more')
|
||||||
max_retry = Parameter('maximum number of retries', IntRange(0, 99), readonly=False, default=0)
|
max_retry = Parameter('maximum number of retries', IntRange(0, 99), readonly=False, default=0)
|
||||||
stall_thr = Parameter('stallGuard threshold', IntRange(-64, 63), readonly=False, value=0)
|
stall_thr = Parameter('stallGuard threshold', IntRange(-64, 63), readonly=False, value=0)
|
||||||
|
input_bits = Parameter('input bits', IntRange(0, 255), group='more', export=False)
|
||||||
|
input1 = Parameter('input 1', BoolType(), export=False, group='more')
|
||||||
|
input2 = Parameter('input 2', BoolType(), export=False, group='more')
|
||||||
|
input3 = Parameter('input 3', BoolType(), export=False, group='more')
|
||||||
|
output0 = Parameter('output 0', BoolType(), readonly=False, export=False, group='more', default=0)
|
||||||
|
output1 = Parameter('output 1', BoolType(), readonly=False, export=False, group='more', default=0)
|
||||||
|
home = Parameter('state of home switch (input 3)', BoolType(), group='more', export=False)
|
||||||
|
has_home = Property('enable home and activate pullup resistor', BoolType(), export=False,
|
||||||
|
default=True)
|
||||||
|
has_inputs = Property('inputs are polled', BoolType(), export=False,
|
||||||
|
default=False)
|
||||||
|
with_pullup = Property('activate pullup', BoolType(), export=False,
|
||||||
|
default=True)
|
||||||
|
|
||||||
pollinterval = Parameter(group='more')
|
pollinterval = Parameter(group='more')
|
||||||
target_min = Limit()
|
target_min = Limit()
|
||||||
target_max = Limit()
|
target_max = Limit()
|
||||||
@ -145,6 +157,18 @@ class Motor(PersistentMixin, HasIO, Drivable):
|
|||||||
_loading = False # True when loading parameters
|
_loading = False # True when loading parameters
|
||||||
_drv_try = 0
|
_drv_try = 0
|
||||||
|
|
||||||
|
def checkProperties(self):
|
||||||
|
super().checkProperties()
|
||||||
|
if self.has_home:
|
||||||
|
self.parameters['home'].export = '_home'
|
||||||
|
self.setProperty('has_inputs', True)
|
||||||
|
if not self.has_inputs:
|
||||||
|
self.setProperty('with_pullup', False)
|
||||||
|
|
||||||
|
def writeInitParams(self):
|
||||||
|
super().writeInitParams()
|
||||||
|
self.comm(SET_IO, 0, self.with_pullup)
|
||||||
|
|
||||||
def comm(self, cmd, adr, value=0, bank=0):
|
def comm(self, cmd, adr, value=0, bank=0):
|
||||||
"""set or get a parameter
|
"""set or get a parameter
|
||||||
|
|
||||||
@ -402,13 +426,6 @@ class Motor(PersistentMixin, HasIO, Drivable):
|
|||||||
def read_steppos(self):
|
def read_steppos(self):
|
||||||
return self._read_axispar(STEPPOS_ADR, ANGLE_SCALE) + self.zero
|
return self._read_axispar(STEPPOS_ADR, ANGLE_SCALE) + self.zero
|
||||||
|
|
||||||
def read_home(self):
|
|
||||||
return not self.comm(GET_IO, 255) & 8
|
|
||||||
|
|
||||||
def write_has_home(self, value):
|
|
||||||
"""activate pullup resistor"""
|
|
||||||
return bool(self.comm(SET_IO, 0, value))
|
|
||||||
|
|
||||||
@Command(FloatRange())
|
@Command(FloatRange())
|
||||||
def set_zero(self, value):
|
def set_zero(self, value):
|
||||||
"""adapt zero to make current position equal to given value"""
|
"""adapt zero to make current position equal to given value"""
|
||||||
@ -459,3 +476,47 @@ class Motor(PersistentMixin, HasIO, Drivable):
|
|||||||
def set_axis_par(self, adr, value):
|
def set_axis_par(self, adr, value):
|
||||||
"""set arbitrary motor parameter"""
|
"""set arbitrary motor parameter"""
|
||||||
return self.comm(SET_AXIS_PAR, adr, value)
|
return self.comm(SET_AXIS_PAR, adr, value)
|
||||||
|
|
||||||
|
def read_input_bits(self):
|
||||||
|
if not self.has_inputs:
|
||||||
|
return 0
|
||||||
|
bits = self.comm(GET_IO, 255)
|
||||||
|
self.input1 = bool(bits & 2)
|
||||||
|
self.input2 = bool(bits & 4)
|
||||||
|
self.input3 = bool(bits & 8)
|
||||||
|
self.home = not self.input3
|
||||||
|
return bits
|
||||||
|
|
||||||
|
@nopoll
|
||||||
|
def read_home(self):
|
||||||
|
self.read_input_bits()
|
||||||
|
return self.home
|
||||||
|
|
||||||
|
@nopoll
|
||||||
|
def read_input1(self):
|
||||||
|
self.read_input_bits()
|
||||||
|
return self.input1
|
||||||
|
|
||||||
|
@nopoll
|
||||||
|
def read_input2(self):
|
||||||
|
self.read_input_bits()
|
||||||
|
return self.input2
|
||||||
|
|
||||||
|
@nopoll
|
||||||
|
def read_input3(self):
|
||||||
|
self.read_input_bits()
|
||||||
|
return self.input3
|
||||||
|
|
||||||
|
def write_output0(self, value):
|
||||||
|
return self.comm(SET_IO, 0, value, bank=2)
|
||||||
|
|
||||||
|
@nopoll
|
||||||
|
def read_output0(self):
|
||||||
|
return self.comm(GET_IO, 0, bank=2)
|
||||||
|
|
||||||
|
def write_output1(self, value):
|
||||||
|
return self.comm(SET_IO, 1, value, bank=2)
|
||||||
|
|
||||||
|
@nopoll
|
||||||
|
def read_output1(self):
|
||||||
|
return self.comm(GET_IO, 1, bank=2)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user