branch develop: recent changes from branch wip
Change-Id: I2e1173423f2aa164a8a7158921b354c2aff1ab2c
This commit is contained in:
@ -25,10 +25,10 @@ import math
|
||||
import re
|
||||
import time
|
||||
|
||||
from frappy.core import Drivable, HasIO, Writable, StatusType, \
|
||||
from frappy.core import Command, Drivable, HasIO, Writable, StatusType, \
|
||||
Parameter, Property, Readable, StringIO, Attached, IDLE, RAMPING, nopoll
|
||||
from frappy.datatypes import EnumType, FloatRange, StringType, StructOf, BoolType, TupleOf
|
||||
from frappy.errors import HardwareError, ProgrammingError, ConfigError, RangeError
|
||||
from frappy.errors import HardwareError, ProgrammingError, ConfigError
|
||||
from frappy_psi.convergence import HasConvergence
|
||||
from frappy.states import Retry, Finish
|
||||
from frappy.mixins import HasOutputModule, HasControlledBy
|
||||
@ -218,7 +218,6 @@ class HasInput(HasControlledBy, MercuryChannel):
|
||||
class Loop(HasOutputModule, MercuryChannel, Drivable):
|
||||
"""common base class for loops"""
|
||||
output_module = Attached(HasInput, mandatory=False)
|
||||
control_active = Parameter(readonly=False)
|
||||
ctrlpars = Parameter(
|
||||
'pid (proportional band, integral time, differential time',
|
||||
StructOf(p=FloatRange(0, unit='$'), i=FloatRange(0, unit='min'), d=FloatRange(0, unit='min')),
|
||||
@ -226,14 +225,15 @@ class Loop(HasOutputModule, MercuryChannel, Drivable):
|
||||
)
|
||||
enable_pid_table = Parameter('', BoolType(), readonly=False)
|
||||
|
||||
def set_output(self, active, source='HW'):
|
||||
def set_output(self, active, source=None):
|
||||
if active:
|
||||
self.activate_control()
|
||||
else:
|
||||
self.deactivate_control(source)
|
||||
|
||||
def set_target(self, target):
|
||||
self.set_output(True)
|
||||
if not self.control_active:
|
||||
self.activate_control()
|
||||
self.target = target
|
||||
|
||||
def read_enable_pid_table(self):
|
||||
@ -254,9 +254,16 @@ class Loop(HasOutputModule, MercuryChannel, Drivable):
|
||||
def read_status(self):
|
||||
return IDLE, ''
|
||||
|
||||
@Command()
|
||||
def control_off(self):
|
||||
"""switch control off"""
|
||||
# remark: this is needed in frappy_psi.trition.TemperatureLoop, as the heater
|
||||
# output is not available there. We define it here as a convenience for the user.
|
||||
self.write_control_active(False)
|
||||
|
||||
|
||||
class ConvLoop(HasConvergence, Loop):
|
||||
def deactivate_control(self, source):
|
||||
def deactivate_control(self, source=None):
|
||||
if self.control_active:
|
||||
super().deactivate_control(source)
|
||||
self.convergence_state.start(self.inactive_state)
|
||||
@ -372,16 +379,14 @@ class TemperatureLoop(TemperatureSensor, ConvLoop):
|
||||
super().doPoll()
|
||||
self.read_setpoint()
|
||||
|
||||
def read_control_active(self):
|
||||
active = self.query(f'DEV::{self.ENABLE}', off_on)
|
||||
self.set_output(active)
|
||||
return active
|
||||
def set_control_active(self, active):
|
||||
super().set_control_active(active)
|
||||
self.change(f'DEV::{self.ENABLE}', active, off_on)
|
||||
|
||||
def write_control_active(self, value):
|
||||
if value:
|
||||
raise RangeError('write to target to switch control on')
|
||||
self.set_output(value, 'user')
|
||||
return self.change(f'DEV::{self.ENABLE}', value, off_on)
|
||||
def initialReads(self):
|
||||
# initialize control active from HW
|
||||
active = self.query(f'DEV::{self.ENABLE}', off_on)
|
||||
super().set_output(active, 'HW')
|
||||
|
||||
@nopoll # polled by read_setpoint
|
||||
def read_target(self):
|
||||
@ -413,7 +418,7 @@ class TemperatureLoop(TemperatureSensor, ConvLoop):
|
||||
self.change(f'DEV::{self.ENABLE}', True, off_on)
|
||||
super().set_target(target)
|
||||
|
||||
def deactivate_control(self, source):
|
||||
def deactivate_control(self, source=None):
|
||||
if self.__ramping:
|
||||
self.__ramping = False
|
||||
# stop ramping setpoint
|
||||
@ -508,14 +513,16 @@ class PressureLoop(PressureSensor, HasControlledBy, ConvLoop):
|
||||
output_module = Attached(ValvePos, mandatory=False)
|
||||
tolerance = Parameter(default=0.1)
|
||||
|
||||
def read_control_active(self):
|
||||
active = self.query('DEV::PRES:LOOP:FAUT', off_on)
|
||||
self.set_output(active)
|
||||
return active
|
||||
def set_control_active(self, active):
|
||||
super().set_control_active(active)
|
||||
if not active:
|
||||
self.self_controlled() # switches off auto flow
|
||||
return self.change('DEV::PRES:LOOP:FAUT', active, off_on)
|
||||
|
||||
def write_control_active(self, value):
|
||||
self.set_output(value, 'user')
|
||||
return self.change('DEV::PRES:LOOP:FAUT', value, off_on)
|
||||
def initialReads(self):
|
||||
# initialize control active from HW
|
||||
active = self.query('DEV::PRES:LOOP:FAUT', off_on)
|
||||
super().set_output(active, 'HW')
|
||||
|
||||
def read_target(self):
|
||||
return self.query('DEV::PRES:LOOP:PRST')
|
||||
@ -560,14 +567,15 @@ class HasAutoFlow:
|
||||
if value:
|
||||
self.needle_valve.controlled_by = self.name
|
||||
else:
|
||||
if self.needle_valve.control_active:
|
||||
self.needle_valve.set_target(self.flowpars[1][0]) # flow min
|
||||
if self.needle_valve.controlled_by != SELF:
|
||||
self.needle_valve.controlled_by = SELF
|
||||
self.needle_valve.write_target(self.flowpars[1][0]) # flow min
|
||||
return value
|
||||
|
||||
def auto_flow_off(self):
|
||||
def auto_flow_off(self, source=None):
|
||||
if self.auto_flow:
|
||||
self.log.warning('switch auto flow off')
|
||||
self.log.warning(f'switched auto flow off by {source or self.name}')
|
||||
self.write_auto_flow(False)
|
||||
|
||||
|
||||
|
@ -83,7 +83,7 @@ class Motor(HasOffset, HasStates, PersistentMixin, HasIO, Drivable):
|
||||
|
||||
ioClass = PhytronIO
|
||||
_step_size = None # degree / step
|
||||
_blocking_error = None # None or a string indicating the reason of an error needing reset
|
||||
_blocking_error = None # None or a string indicating the reason of an error needing clear_errors
|
||||
_running = False # status indicates motor is running
|
||||
|
||||
STATUS_MAP = {
|
||||
@ -121,10 +121,10 @@ class Motor(HasOffset, HasStates, PersistentMixin, HasIO, Drivable):
|
||||
if not axisbit & active_axes: # power cycle detected and this axis not yet active
|
||||
self.set('P37S', axisbit | active_axes) # activate axis
|
||||
if now < self.alive_time + 7 * 24 * 3600: # the device was running within last week
|
||||
# inform the user about the loss of position by the need of doing reset_error
|
||||
# inform the user about the loss of position by the need of doing clear_errors
|
||||
self._blocking_error = 'lost position'
|
||||
else: # do reset silently
|
||||
self.reset_error()
|
||||
else: # do silently
|
||||
self.clear_errors()
|
||||
self.alive_time = now
|
||||
self.saveParameters()
|
||||
return now
|
||||
@ -171,7 +171,7 @@ class Motor(HasOffset, HasStates, PersistentMixin, HasIO, Drivable):
|
||||
def write_target(self, value):
|
||||
self.read_alive_time()
|
||||
if self._blocking_error:
|
||||
self.status = ERROR, 'reset needed after ' + self._blocking_error
|
||||
self.status = ERROR, 'clear_errors needed after ' + self._blocking_error
|
||||
raise HardwareError(self.status[1])
|
||||
self.saveParameters()
|
||||
if self.backlash:
|
||||
@ -261,7 +261,7 @@ class Motor(HasOffset, HasStates, PersistentMixin, HasIO, Drivable):
|
||||
self.start_machine(self.stopping, status=(BUSY, 'stopping'))
|
||||
|
||||
@Command
|
||||
def reset_error(self):
|
||||
def clear_errors(self):
|
||||
"""Reset error, set position to encoder"""
|
||||
self.read_value()
|
||||
if self._blocking_error:
|
||||
@ -286,3 +286,4 @@ class Motor(HasOffset, HasStates, PersistentMixin, HasIO, Drivable):
|
||||
self.read_value()
|
||||
self.status = 'IDLE', 'after error reset'
|
||||
self._blocking_error = None
|
||||
self.target = self.value # clear error in target
|
||||
|
@ -383,6 +383,19 @@ SEA_TO_SECOPTYPE = {
|
||||
}
|
||||
|
||||
|
||||
class SeaEnum(EnumType):
|
||||
"""some sea enum nodes have text type -> accept '<integer>' also"""
|
||||
def copy(self):
|
||||
return SeaEnum(self._enum)
|
||||
|
||||
def __call__(self, value):
|
||||
try:
|
||||
value = int(value)
|
||||
except TypeError:
|
||||
pass
|
||||
return super().__call__(value)
|
||||
|
||||
|
||||
def get_datatype(paramdesc):
|
||||
typ = paramdesc['type']
|
||||
result = SEA_TO_SECOPTYPE.get(typ, False)
|
||||
@ -390,7 +403,7 @@ def get_datatype(paramdesc):
|
||||
return result
|
||||
# special cases
|
||||
if typ == 'enum':
|
||||
return EnumType(paramdesc['enum'])
|
||||
return SeaEnum(paramdesc['enum'])
|
||||
raise ValueError('unknown SEA type %r' % typ)
|
||||
|
||||
|
||||
@ -492,9 +505,7 @@ class SeaModule(Module):
|
||||
raise ConfigError(f"{sea_object}/{paramdesc['path']} is not writable")
|
||||
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())
|
||||
extra_module_set = set(cfgdict.pop('extra_modules', ()))
|
||||
path2param = {}
|
||||
attributes = {'sea_object': sea_object, 'path2param': path2param}
|
||||
|
||||
|
@ -25,7 +25,7 @@ from frappy.core import Writable, Parameter, Readable, Drivable, IDLE, WARN, BUS
|
||||
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_psi.mercury import MercuryChannel, Mapped, off_on, HasInput
|
||||
from frappy_psi import mercury
|
||||
|
||||
actions = Enum(none=0, condense=1, circulate=2, collect=3)
|
||||
@ -256,15 +256,14 @@ class TemperatureLoop(ScannerChannel, mercury.TemperatureLoop):
|
||||
ctrlpars = Parameter('pid (gain, integral (inv. time), differential time')
|
||||
system_channel = Property('system channel name', StringType(), 'MC')
|
||||
|
||||
def write_control_active(self, value):
|
||||
def set_control_active(self, active):
|
||||
if self.system_channel:
|
||||
self.change('SYS:DR:CHAN:%s' % self.system_channel, self.slot.split(',')[0], str)
|
||||
if value:
|
||||
if active:
|
||||
self.change('DEV::TEMP:LOOP:FILT:ENAB', 'ON', str)
|
||||
if self.output_module:
|
||||
limit = self.output_module.read_limit() or None # None: max. limit
|
||||
limit = self.output_module.read_limit()
|
||||
self.output_module.write_limit(limit)
|
||||
return super().write_control_active(value)
|
||||
|
||||
|
||||
class HeaterOutput(HasInput, MercuryChannel, Writable):
|
||||
@ -286,7 +285,7 @@ class HeaterOutput(HasInput, MercuryChannel, Writable):
|
||||
return self.value
|
||||
|
||||
def write_target(self, value):
|
||||
self.write_controlled_by(SELF)
|
||||
self.self_controlled()
|
||||
if self.resistivity:
|
||||
# round to the next voltage step
|
||||
value = round(sqrt(value * self.resistivity)) ** 2 / self.resistivity
|
||||
@ -301,13 +300,12 @@ class HeaterOutputWithRange(HeaterOutput):
|
||||
|
||||
def read_limit(self):
|
||||
maxcur = self.query('DEV::TEMP:LOOP:RANGE') # mA
|
||||
if maxcur == 0:
|
||||
maxcur = 100 # 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())
|
||||
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
|
||||
|
Reference in New Issue
Block a user