diff --git a/secop/params.py b/secop/params.py index 03017b4..05f99d2 100644 --- a/secop/params.py +++ b/secop/params.py @@ -216,6 +216,9 @@ class Parameter(Accessible): self.export = '_' + self.name else: raise ProgrammingError('can not use %r as name of a Parameter' % self.name) + if 'export' in self.ownProperties: + # avoid export=True overrides export= + self.ownProperties['export'] = self.export def copy(self): """return a (deep) copy of ourselfs""" @@ -381,6 +384,9 @@ class Command(Accessible): self.export = '_' + name else: raise ProgrammingError('can not use %r as name of a Command' % name) from None + if 'export' in self.ownProperties: + # avoid export=True overrides export= + self.ownProperties['export'] = self.export if not self._inherit: for key, pobj in self.properties.items(): if key not in self.propertyValues: diff --git a/secop/rwhandler.py b/secop/rwhandler.py index cadbdc1..cd35009 100644 --- a/secop/rwhandler.py +++ b/secop/rwhandler.py @@ -105,7 +105,8 @@ class ReadHandler(Handler): wrapped.wrapped = True method = 'read_' + key - if hasattr(owner, method): + rfunc = getattr(owner, method, None) + if rfunc and not rfunc.wrapped: raise ProgrammingError('superfluous method %s.%s (overwritten by ReadHandler)' % (owner.__name__, method)) setattr(owner, method, wrapped) @@ -129,7 +130,56 @@ class WriteHandler(Handler): wrapped.wrapped = True method = 'write_' + key - if hasattr(owner, method): + wfunc = getattr(owner, method, None) + if wfunc and not wfunc.wrapped: + raise ProgrammingError('superfluous method %s.%s (overwritten by WriteHandler)' + % (owner.__name__, method)) + setattr(owner, method, wrapped) + + +class WriteParameters(dict): + def __init__(self, modobj): + super().__init__() + self.obj = modobj + + def __missing__(self, key): + try: + return self.obj.writeDict.pop(key) + except KeyError: + return getattr(self.obj, key) + + def as_tuple(self, *keys): + """return values of given keys as a tuple""" + return tuple(self[k] for k in keys) + + +class MultiWriteHandler(Handler): + """decorator for common write handler + + calls the wrapped write method function with values as an argument. + - values[pname] returns the to be written value + - values['key'] returns a value taken from writeDict + - or, if not available return obj.key + - values.as_tuple() returns a tuple with the items in the same order as keys + """ + + def __set_name__(self, owner, name): + """create the wrapped write_* methods""" + + self.method_names.discard(self.func.__qualname__) + for key in self.keys: + + @wraps(self.func) + def wrapped(module, value, pname=key, func=self.func): + values = WriteParameters(module) + values[pname] = value + func(module, values) + return Done + + wrapped.wrapped = True + method = 'write_' + key + wfunc = getattr(owner, method, None) + if wfunc and not wfunc.wrapped: raise ProgrammingError('superfluous method %s.%s (overwritten by WriteHandler)' % (owner.__name__, method)) setattr(owner, method, wrapped) diff --git a/secop_psi/ppms.py b/secop_psi/ppms.py index 60459b8..dfd536f 100644 --- a/secop_psi/ppms.py +++ b/secop_psi/ppms.py @@ -33,8 +33,8 @@ Polling of value and status is done commonly for all modules. For each registere import threading import time +from ast import literal_eval # convert string as comma separated numbers into tuple -import secop.iohandler from secop.datatypes import BoolType, EnumType, \ FloatRange, IntRange, StatusType, StringType from secop.errors import HardwareError @@ -44,6 +44,7 @@ from secop.modules import Communicator, Done, \ Drivable, Parameter, Property, Readable from secop.poller import Poller from secop.io import HasIO +from secop.rwhandler import ReadHandler, MultiWriteHandler try: import secop_psi.ppmswindows as ppmshw @@ -52,23 +53,6 @@ except ImportError: import secop_psi.ppmssim as ppmshw -class IOHandler(secop.iohandler.IOHandler): - """IO handler for PPMS commands - - deals with typical format: - - - query command: ``?`` - - reply: ``,, ..`` - - change command: `` ,,...`` - """ - CMDARGS = ['no'] # the channel number is needed in channel commands - CMDSEPARATOR = None # no command chaining - - def __init__(self, name, querycmd, replyfmt): - changecmd = querycmd.split('?')[0] + ' ' - super().__init__(name, querycmd, replyfmt, changecmd) - - class Main(Communicator): """ppms communicator module""" @@ -180,15 +164,10 @@ class Channel(PpmsBase): if not self.channel: self.channel = self.name - def get_settings(self, pname): - return '' - class UserChannel(Channel): """user channel""" - # pollinterval = Parameter(visibility=3) - no = Property('channel number', datatype=IntRange(0, 0), export=False, default=0) linkenable = Property('name of linked channel for enabling', @@ -204,71 +183,83 @@ class UserChannel(Channel): class DriverChannel(Channel): """driver channel""" - drvout = IOHandler('drvout', 'DRVOUT? %(no)d', '%d,%g,%g') - - current = Parameter('driver current', readonly=False, handler=drvout, + current = Parameter('driver current', readonly=False, poll=True, # poll only one parameter datatype=FloatRange(0., 5000., unit='uA')) - powerlimit = Parameter('power limit', readonly=False, handler=drvout, + powerlimit = Parameter('power limit', readonly=False, datatype=FloatRange(0., 1000., unit='uW')) - # pollinterval = Parameter(visibility=3) - def analyze_drvout(self, no, current, powerlimit): + param_names = 'current', 'powerlimit' + + @ReadHandler(param_names) + def read_params(self, pname=None): + no, self.current, self.powerlimit = literal_eval( + self.communicate('DRVOUT? %d' % self.no)) if self.no != no: raise HardwareError('DRVOUT command: channel number in reply does not match') - return dict(current=current, powerlimit=powerlimit) + return Done - def change_drvout(self, change): - change.readValues() - return change.current, change.powerlimit + @MultiWriteHandler(param_names) + def write_params(self, values): + """write parameters + + :param values: a dict like object containing the parameters to be written + """ + self.read_params() # make sure parameters are up to date + self.set_params('DRVOUT %(no)d,%(current)g,%(powerlimit)g' % values) + return self.read_params(None) # read back class BridgeChannel(Channel): """bridge channel""" - bridge = IOHandler('bridge', 'BRIDGE? %(no)d', '%d,%g,%g,%d,%d,%g') - # pylint: disable=invalid-name - ReadingMode = Enum('ReadingMode', standard=0, fast=1, highres=2) - - enabled = Parameter(handler=bridge) - excitation = Parameter('excitation current', readonly=False, handler=bridge, + excitation = Parameter('excitation current', readonly=False, poll=True, # poll only one parameter datatype=FloatRange(0.01, 5000., unit='uA')) - powerlimit = Parameter('power limit', readonly=False, handler=bridge, + powerlimit = Parameter('power limit', readonly=False, datatype=FloatRange(0.001, 1000., unit='uW')) - dcflag = Parameter('True when excitation is DC (else AC)', readonly=False, handler=bridge, + dcflag = Parameter('True when excitation is DC (else AC)', readonly=False, datatype=BoolType()) - readingmode = Parameter('reading mode', readonly=False, handler=bridge, - datatype=EnumType(ReadingMode)) - voltagelimit = Parameter('voltage limit', readonly=False, handler=bridge, + readingmode = Parameter('reading mode', readonly=False, + datatype=EnumType(standard=0, fast=1, highres=2)) + voltagelimit = Parameter('voltage limit', readonly=False, datatype=FloatRange(0.0001, 100., unit='mV')) - # pollinterval = Parameter(visibility=3) - def analyze_bridge(self, no, excitation, powerlimit, dcflag, readingmode, voltagelimit): + param_names = 'enabled', 'enabled', 'powerlimit', 'dcflag', 'readingmode', 'voltagelimit' + + @ReadHandler(param_names) + def read_params(self, pname=None): + no, excitation, powerlimit, self.dcflag, self.readingmode, voltagelimit = literal_eval( + self.communicate('BRIDGE? %d' % self.no)) if self.no != no: raise HardwareError('DRVOUT command: channel number in reply does not match') - return dict( - enabled=excitation != 0 and powerlimit != 0 and voltagelimit != 0, - excitation=excitation or self.excitation, - powerlimit=powerlimit or self.powerlimit, - dcflag=dcflag, - readingmode=readingmode, - voltagelimit=voltagelimit or self.voltagelimit, - ) + self.enabled = excitation != 0 and powerlimit != 0 and voltagelimit != 0 + if excitation: + self.excitation = excitation + if powerlimit: + self.powerlimit = powerlimit + if voltagelimit: + self.voltagelimit = voltagelimit + return Done - def change_bridge(self, change): - change.readValues() - if change.enabled: - return self.no, change.excitation, change.powerlimit, change.dcflag, change.readingmode, change.voltagelimit - return self.no, 0, 0, change.dcflag, change.readingmode, 0 + @MultiWriteHandler(param_names) + def write_params(self, values): + """write parameters + + :param values: a dict like object containing the parameters to be written + """ + self.read_params() # make sure parameters are up to date + if not values['enabled']: + values['excitation'] = 0 + values['powerlimit'] = 0 + values['voltagelimit'] = 0 + assert self.communicate('BRIDGE %(no)d,%(enabled)g,%(powerlimit)g,%(dcflag)d,' + '%(readingmode)d,%(voltagelimit)g' % values) == 'OK' + return self.read_params() # read back class Level(PpmsBase): """helium level""" - level = IOHandler('level', 'LEVEL?', '%g,%d') - - value = Parameter(datatype=FloatRange(unit='%'), handler=level) - status = Parameter(handler=level) - # pollinterval = Parameter(visibility=3) + value = Parameter(datatype=FloatRange(unit='%'), poll=True) channel = 'level' @@ -278,10 +269,9 @@ class Level(PpmsBase): # when called from Main.read_data, value is always None # value and status is polled via settings - def analyze_level(self, level, status): + def read_value(self): # ignore 'old reading' state of the flag, as this happens only for a short time - # during measuring - return dict(value=level, status=(self.Status.IDLE, '')) + return literal_eval(self.communicate('LEVEL?'))[0] class Chamber(PpmsBase, Drivable): @@ -290,115 +280,74 @@ class Chamber(PpmsBase, Drivable): value is an Enum, which is redundant with the status text """ - chamber = IOHandler('chamber', 'CHAMBER?', '%d') Status = Drivable.Status - # pylint: disable=invalid-name - Operation = Enum( - 'Operation', - seal_immediately=0, - purge_and_seal=1, - vent_and_seal=2, - pump_continuously=3, - vent_continuously=4, - hi_vacuum=5, - noop=10, - ) - StatusCode = Enum( - 'StatusCode', - unknown=0, - purged_and_sealed=1, - vented_and_sealed=2, - sealed_unknown=3, - purge_and_seal=4, - vent_and_seal=5, - pumping_down=6, - at_hi_vacuum=7, - pumping_continuously=8, - venting_continuously=9, - general_failure=15, - ) - - value = Parameter(description='chamber state', # handler=chamber, - datatype=EnumType(StatusCode)) - target = Parameter(description='chamber command', handler=chamber, - datatype=EnumType(Operation)) - # pollinterval = Parameter(visibility=3) - - STATUS_MAP = { - StatusCode.purged_and_sealed: (Status.IDLE, 'purged and sealed'), - StatusCode.vented_and_sealed: (Status.IDLE, 'vented and sealed'), - StatusCode.sealed_unknown: (Status.WARN, 'sealed unknown'), - StatusCode.purge_and_seal: (Status.BUSY, 'purge and seal'), - StatusCode.vent_and_seal: (Status.BUSY, 'vent and seal'), - StatusCode.pumping_down: (Status.BUSY, 'pumping down'), - StatusCode.at_hi_vacuum: (Status.IDLE, 'at hi vacuum'), - StatusCode.pumping_continuously: (Status.IDLE, 'pumping continuously'), - StatusCode.venting_continuously: (Status.IDLE, 'venting continuously'), - StatusCode.general_failure: (Status.ERROR, 'general failure'), - } + code_table = [ + # valuecode, status, statusname, opcode, targetname + (0, Status.IDLE, 'unknown', 10, 'noop'), + (1, Status.IDLE, 'purged_and_sealed', 1, 'purge_and_seal'), + (2, Status.IDLE, 'vented_and_sealed', 2, 'vent_and_seal'), + (3, Status.WARN, 'sealed_unknown', 0, 'seal_immediately'), + (4, Status.BUSY, 'purge_and_seal', None, None), + (5, Status.BUSY, 'vent_and_seal', None, None), + (6, Status.BUSY, 'pumping_down', None, None), + (8, Status.IDLE, 'pumping_continuously', 3, 'pump_continuously'), + (9, Status.IDLE, 'venting_continuously', 4, 'vent_continuously'), + (15, Status.ERROR, 'general_failure', None, None), + ] + value_codes = {k: v for v, _, k, _, _ in code_table} + target_codes = {k: v for v, _, _, _, k in code_table if k} + name2opcode = {k: v for _, _, _, v, k in code_table if k} + opcode2name = {v: k for _, _, _, v, k in code_table if k} + status_map = {v: (c, k.replace('_', ' ')) for v, c, k, _, _ in code_table} + value = Parameter(description='chamber state', datatype=EnumType(**value_codes), default=0, poll=True) + target = Parameter(description='chamber command', datatype=EnumType(**target_codes), default='noop', poll=True) channel = 'chamber' def update_value_status(self, value, packed_status): status_code = (packed_status >> 8) & 0xf - if status_code in self.STATUS_MAP: + if status_code in self.status_map: self.value = status_code - self.status = self.STATUS_MAP[status_code] + self.status = self.status_map[status_code] else: - self.value = self.StatusCode.unknown + self.value = self.value_map['unknown'] self.status = (self.Status.ERROR, 'unknown status code %d' % status_code) - def analyze_chamber(self, target): - return dict(target=target) + def read_target(self): + opcode = int(self.communicate('CHAMBER?')) + return self.opcode2name[opcode] - def change_chamber(self, change): - # write settings, combining = and current attributes - # and request updated settings - if change.target == self.Operation.noop: - return None - return (change.target,) + def write_target(self, value): + if value == self.target.noop: + return self.target.noop + opcode = self.name2opcode[self.target.enum(value).name] + assert self.communicate('CHAMBER %d' % opcode) == 'OK' + return self.read_target() class Temp(PpmsBase, Drivable): """temperature""" - temp = IOHandler('temp', 'TEMP?', '%g,%g,%d') Status = Enum( Drivable.Status, RAMPING=370, STABILIZING=380, ) - # pylint: disable=invalid-name - ApproachMode = Enum('ApproachMode', fast_settle=0, no_overshoot=1) - value = Parameter(datatype=FloatRange(unit='K')) status = Parameter(datatype=StatusType(Status)) target = Parameter(datatype=FloatRange(1.7, 402.0, unit='K'), poll=False, needscfg=False) setpoint = Parameter('intermediate set point', - datatype=FloatRange(1.7, 402.0, unit='K'), handler=temp) + datatype=FloatRange(1.7, 402.0, unit='K'), poll=True) # poll only one parameter ramp = Parameter('ramping speed', readonly=False, default=0, datatype=FloatRange(0, 20, unit='K/min')) workingramp = Parameter('intermediate ramp value', - datatype=FloatRange(0, 20, unit='K/min'), handler=temp) - approachmode = Parameter('how to approach target!', readonly=False, handler=temp, - datatype=EnumType(ApproachMode)) - # pollinterval = Parameter(visibility=3) + datatype=FloatRange(0, 20, unit='K/min'), default=0) + approachmode = Parameter('how to approach target!', readonly=False, + datatype=EnumType(fast_settle=0, no_overshoot=1), default=0) timeout = Parameter('drive timeout, in addition to ramp time', readonly=False, datatype=FloatRange(0, unit='sec'), default=3600) - - # pylint: disable=invalid-name - TempStatus = Enum( - 'TempStatus', - stable_at_target=1, - changing=2, - within_tolerance=5, - outside_tolerance=6, - filling_emptying_reservoir=7, - standby=10, - control_disabled=13, - can_not_complete=14, - general_failure=15, - ) + general_stop = Property('respect general stop', datatype=BoolType(), + default=True, value=False) STATUS_MAP = { 1: (Status.IDLE, 'stable at target'), 2: (Status.RAMPING, 'ramping'), @@ -410,8 +359,6 @@ class Temp(PpmsBase, Drivable): 14: (Status.ERROR, 'can not complete'), 15: (Status.ERROR, 'general failure'), } - general_stop = Property('respect general stop', datatype=BoolType(), - default=True, value=False) channel = 'temp' _stopped = False @@ -422,6 +369,43 @@ class Temp(PpmsBase, Drivable): _wait_at10 = False _ramp_at_limit = False + param_names = 'setpoint', 'workingramp', 'approachmode' + + @ReadHandler(param_names) + def read_params(self, pname): + settings = literal_eval(self.communicate('TEMP?')) + if settings == self._last_settings: + # update parameters only on change, as 'ramp' and 'approachmode' are + # not always sent to the hardware + return Done + self.setpoint, self.workingramp, self.approachmode = self._last_settings = settings + if self.setpoint != 10 or not self._wait_at10: + self.log.debug('read back target %g %r' % (self.setpoint, self._wait_at10)) + self.target = self.setpoint + if self.workingramp != 2 or not self._ramp_at_limit: + self.log.debug('read back ramp %g %r' % (self.workingramp, self._ramp_at_limit)) + self.ramp = self.workingramp + return Done + + def _write_params(self, setpoint, ramp, approachmode): + wait_at10 = False + ramp_at_limit = False + if self.value > 11: + if setpoint <= 10: + wait_at10 = True + setpoint = 10 + elif self.value > setpoint: + if ramp >= 2: + ramp = 2 + ramp_at_limit = True + self._wait_at10 = wait_at10 + self._ramp_at_limit = ramp_at_limit + self.calc_expected(setpoint, ramp) + self.log.debug( + 'change_temp v %r s %r r %r w %r l %r' % (self.value, setpoint, ramp, wait_at10, ramp_at_limit)) + assert self.communicate('TEMP %g,%g,%d' % (setpoint, ramp, approachmode)) == 'OK' + self.read_params('') + def update_value_status(self, value, packed_status): if value is None: self.status = (self.Status.ERROR, 'invalid value') @@ -439,7 +423,7 @@ class Temp(PpmsBase, Drivable): if now > self._cool_deadline: self._wait_at10 = False self._last_change = now - self.temp.write(self, 'setpoint', self.target) + self._write_params(self.target, self.ramp, self.approachmode) status = (self.Status.STABILIZING, 'waiting at 10 K') if self._last_change: # there was a change, which is not yet confirmed by hw if now > self._last_change + 5: @@ -468,41 +452,6 @@ class Temp(PpmsBase, Drivable): self._expected_target_time = 0 self.status = status - def analyze_temp(self, setpoint, workingramp, approachmode): - if (setpoint, workingramp, approachmode) == self._last_settings: - # update parameters only on change, as 'ramp' and 'approachmode' are - # not always sent to the hardware - return {} - self._last_settings = setpoint, workingramp, approachmode - if setpoint != 10 or not self._wait_at10: - self.log.debug('read back target %g %r' % (setpoint, self._wait_at10)) - self.target = setpoint - if workingramp != 2 or not self._ramp_at_limit: - self.log.debug('read back ramp %g %r' % (workingramp, self._ramp_at_limit)) - self.ramp = workingramp - result = dict(setpoint=setpoint, workingramp=workingramp, approachmode=approachmode) - self.log.debug('analyze_temp %r %r' % (result, (self.target, self.ramp))) - return result - - def change_temp(self, change): - ramp = change.ramp - setpoint = change.setpoint - wait_at10 = False - ramp_at_limit = False - if self.value > 11: - if setpoint <= 10: - wait_at10 = True - setpoint = 10 - elif self.value > setpoint: - if ramp >= 2: - ramp = 2 - ramp_at_limit = True - self._wait_at10 = wait_at10 - self._ramp_at_limit = ramp_at_limit - self.calc_expected(setpoint, ramp) - self.log.debug('change_temp v %r s %r r %r w %r l %r' % (self.value, setpoint, ramp, wait_at10, ramp_at_limit)) - return setpoint, ramp, change.approachmode - def write_target(self, target): self._stopped = False if abs(self.target - self.value) <= 2e-5 * target and target == self.target: @@ -510,23 +459,23 @@ class Temp(PpmsBase, Drivable): self._status_before_change = self.status self.status = (self.Status.BUSY, 'changed target') self._last_change = time.time() - self.temp.write(self, 'setpoint', target) + self._write_params(target, self.ramp, self.approachmode) self.log.debug('write_target %s' % repr((self.setpoint, target, self._wait_at10))) return target def write_approachmode(self, value): if self.isDriving(): - self.temp.write(self, 'approachmode', value) + self._write_params(self.setpoint, self.ramp, value) return Done self.approachmode = value - return None # do not execute TEMP command, as this would trigger an unnecessary T change + return Done # do not execute TEMP command, as this would trigger an unnecessary T change def write_ramp(self, value): if self.isDriving(): - self.temp.write(self, 'ramp', value) + self._write_params(self.setpoint, value, self.approachmode) return Done - # self.ramp = value - return None # do not execute TEMP command, as this would trigger an unnecessary T change + self.ramp = value + return Done # do not execute TEMP command, as this would trigger an unnecessary T change def calc_expected(self, target, ramp): self._expected_target_time = time.time() + abs(target - self.value) * 60.0 / max(0.1, ramp) @@ -547,35 +496,30 @@ class Temp(PpmsBase, Drivable): class Field(PpmsBase, Drivable): """magnetic field""" - field = IOHandler('field', 'FIELD?', '%g,%g,%d,%d') Status = Enum( Drivable.Status, PREPARED=150, PREPARING=340, RAMPING=370, + STABILIZING=380, FINALIZING=390, ) - # pylint: disable=invalid-name - PersistentMode = Enum('PersistentMode', persistent=0, driven=1) - ApproachMode = Enum('ApproachMode', linear=0, no_overshoot=1, oscillate=2) - value = Parameter(datatype=FloatRange(unit='T')) status = Parameter(datatype=StatusType(Status)) - target = Parameter(datatype=FloatRange(-15, 15, unit='T'), handler=field) - ramp = Parameter('ramping speed', readonly=False, handler=field, - datatype=FloatRange(0.064, 1.19, unit='T/min')) - approachmode = Parameter('how to approach target', readonly=False, handler=field, - datatype=EnumType(ApproachMode)) - persistentmode = Parameter('what to do after changing field', readonly=False, handler=field, - datatype=EnumType(PersistentMode)) - # pollinterval = Parameter(visibility=3) + target = Parameter(datatype=FloatRange(-15, 15, unit='T'), poll=True) # poll only one parameter + ramp = Parameter('ramping speed', readonly=False, + datatype=FloatRange(0.064, 1.19, unit='T/min'), default=0.19) + approachmode = Parameter('how to approach target', readonly=False, + datatype=EnumType(linear=0, no_overshoot=1, oscillate=2), default=0) + persistentmode = Parameter('what to do after changing field', readonly=False, + datatype=EnumType(persistent=0, driven=1), default=0) STATUS_MAP = { 1: (Status.IDLE, 'persistent mode'), 2: (Status.PREPARING, 'switch warming'), 3: (Status.FINALIZING, 'switch cooling'), 4: (Status.IDLE, 'driven stable'), - 5: (Status.FINALIZING, 'driven final'), + 5: (Status.STABILIZING, 'driven final'), 6: (Status.RAMPING, 'charging'), 7: (Status.RAMPING, 'discharging'), 8: (Status.ERROR, 'current error'), @@ -588,6 +532,26 @@ class Field(PpmsBase, Drivable): _last_target = None # last reached target _last_change = 0 # means no target change is pending + param_names = 'target', 'ramp', 'approachmode', 'persistentmode' + + @ReadHandler(param_names) + def read_params(self, pname): + settings = literal_eval(self.communicate('FIELD?')) + # print('last_settings tt %s' % repr(self._last_settings)) + if settings == self._last_settings: + # we update parameters only on change, as 'ramp' and 'approachmode' are + # not always sent to the hardware + return Done + target, ramp, self.approachmode, self.persistentmode = self._last_settings = settings + self.target = round(target * 1e-4, 7) + self.ramp = ramp * 6e-3 + return Done + + def _write_params(self, target, ramp, approachmode, persistentmode): + assert self.communicate('FIELD %g,%g,%d,%d' % ( + target * 1e+4, ramp / 6e-3, approachmode, persistentmode)) == 'OK' + self.read_params('') + def update_value_status(self, value, packed_status): if value is None: self.status = (self.Status.ERROR, 'invalid value') @@ -622,19 +586,6 @@ class Field(PpmsBase, Drivable): status = (status[0], 'stopping (%s)' % status[1]) self.status = status - def analyze_field(self, target, ramp, approachmode, persistentmode): - # print('last_settings tt %s' % repr(self._last_settings)) - if (target, ramp, approachmode, persistentmode) == self._last_settings: - # we update parameters only on change, as 'ramp' and 'approachmode' are - # not always sent to the hardware - return {} - self._last_settings = target, ramp, approachmode, persistentmode - return dict(target=round(target * 1e-4, 7), ramp=ramp * 6e-3, approachmode=approachmode, - persistentmode=persistentmode) - - def change_field(self, change): - return change.target * 1e+4, change.ramp / 6e-3, change.approachmode, change.persistentmode - def write_target(self, target): if abs(self.target - self.value) <= 2e-5 and target == self.target: self.target = target @@ -643,7 +594,7 @@ class Field(PpmsBase, Drivable): self._stopped = False self._last_change = time.time() self.status = (self.Status.BUSY, 'changed target') - self.field.write(self, 'target', target) + self._write_params(target, self.ramp, self.approachmode, self.persistentmode) return Done def write_persistentmode(self, mode): @@ -654,19 +605,19 @@ class Field(PpmsBase, Drivable): self._status_before_change = self.status self._stopped = False self.status = (self.Status.BUSY, 'changed persistent mode') - self.field.write(self, 'persistentmode', mode) + self._write_params(self.target, self.ramp, self.approachmode, mode) return Done def write_ramp(self, value): self.ramp = value if self.isDriving(): - self.field.write(self, 'ramp', value) + self._write_params(self.target, value, self.approachmode, self.persistentmode) return Done return None # do not execute FIELD command, as this would trigger a ramp up of leads current def write_approachmode(self, value): if self.isDriving(): - self.field.write(self, 'approachmode', value) + self._write_params(self.target, self.ramp, value, self.persistentmode) return Done return None # do not execute FIELD command, as this would trigger a ramp up of leads current @@ -684,17 +635,14 @@ class Field(PpmsBase, Drivable): class Position(PpmsBase, Drivable): """rotator position""" - move = IOHandler('move', 'MOVE?', '%g,%g,%g') Status = Drivable.Status value = Parameter(datatype=FloatRange(unit='deg')) - target = Parameter(datatype=FloatRange(-720., 720., unit='deg'), handler=move) + target = Parameter(datatype=FloatRange(-720., 720., unit='deg'), poll=True) # poll only one parameter enabled = Parameter('is this channel used?', readonly=False, poll=False, datatype=BoolType(), default=True) - speed = Parameter('motor speed', readonly=False, handler=move, + speed = Parameter('motor speed', readonly=False, default=12, datatype=FloatRange(0.8, 12, unit='deg/sec')) - # pollinterval = Parameter(visibility=3) - STATUS_MAP = { 1: (Status.IDLE, 'at target'), 5: (Status.BUSY, 'moving'), @@ -709,6 +657,24 @@ class Position(PpmsBase, Drivable): _last_change = 0 _within_target = 0 # time since we are within target + param_names = 'target', 'speed' + + @ReadHandler(param_names) + def read_params(self, pname): + settings = literal_eval(self.communicate('MOVE?')) + if settings == self._last_settings: + # we update parameters only on change, as 'speed' is + # not always sent to the hardware + return Done + self.target, _, speed = self._last_settings = settings + self.speed = (15 - speed) * 0.8 + return Done + + def _write_params(self, target, speed): + speed = int(round(min(14, max(0, 15 - speed / 0.8)), 0)) + assert self.communicate('MOVE %g,%d,%d' % (target, 0, speed)) == 'OK' + return self.read_params('') + def update_value_status(self, value, packed_status): if not self.enabled: self.status = (self.Status.DISABLED, 'disabled') @@ -746,29 +712,17 @@ class Position(PpmsBase, Drivable): status = (status[0], 'stopping (%s)' % status[1]) self.status = status - def analyze_move(self, target, mode, speed): - if (target, speed) == self._last_settings: - # we update parameters only on change, as 'speed' is - # not always sent to the hardware - return {} - self._last_settings = target, speed - return dict(target=target, speed=(15 - speed) * 0.8) - - def change_move(self, change): - speed = int(round(min(14, max(0, 15 - change.speed / 0.8)), 0)) - return change.target, 0, speed - def write_target(self, target): self._stopped = False self._last_change = 0 self._status_before_change = self.status self.status = (self.Status.BUSY, 'changed target') - self.move.write(self, 'target', target) + self._write_params(target, self.speed) return Done def write_speed(self, value): if self.isDriving(): - self.move.write(self, 'speed', value) + self._write_params(self.target, value) return Done self.speed = value return None # do not execute MOVE command, as this would trigger an unnecessary move