From fcad78a682c32f1b74b08dce401b01cee3a67f43 Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Fri, 13 Dec 2019 16:28:22 +0100 Subject: [PATCH] ppms driver now uses command handlers this is a simplicifcation for the ppms driver - the derivation of a changecmd from a querycmd is moved to CmdHandler.__init__ - the special treatment of handlers when writing configured parameters has moved to CmdHandler.write - introduced Drivable.isDriving, changed Module.isBusy Change-Id: I8862ecda9c8cc998bb018bd960f31c9488146707 Reviewed-on: https://forge.frm2.tum.de/review/c/sine2020/secop/playground/+/22033 Tested-by: JenkinsCodeReview Reviewed-by: Markus Zolliker --- secop/commandhandler.py | 83 +++-- secop/metaclass.py | 24 +- secop/modules.py | 28 +- secop_psi/ls370res.py | 7 + secop_psi/ppms.py | 603 +++++++++++------------------------- test/test_commandhandler.py | 6 + 6 files changed, 275 insertions(+), 476 deletions(-) diff --git a/secop/commandhandler.py b/secop/commandhandler.py index 54cc3b7..e1918ce 100755 --- a/secop/commandhandler.py +++ b/secop/commandhandler.py @@ -136,22 +136,26 @@ class CmdParser: return [c(v) for c, v in zip(self.casts, match.groups())] -class ChangeWrapper: - """Wrapper around a module +class Change: + """contains new values for the call to change_ - A ChangeWrapper instance is used as the 'new' argument for the change_ message. + A Change instance is used as the 'new' argument for the change_ method. new. is either the new, changed value or the old value from the module. - In addition '' indicates, whether is to be changed. - setting new. does not yet set the value on the module. + In addition '' in new indicates, whether is to be changed. + new. can not be changed """ - def __init__(self, module, valuedict): - self._module = module - for pname, value in valuedict.items(): - setattr(self, pname, value) + def __init__(self, module, parameters, valuedict): + self.__dict__.update(valuedict, _module=module, _parameters=parameters) - def __getattr__(self, key): + def __getattr__(self, pname): """get current values from _module for unchanged parameters""" - return getattr(self._module, key) + if not pname in self._parameters: + raise AttributeError("parameter '%s' is not within the handlers group" + % pname) + return getattr(self._module, pname) + + def __setattr__(self, pname, value): + raise AttributeError("can't set attribute ") def __contains__(self, pname): """check whether a specific parameter is to be changed""" @@ -237,35 +241,49 @@ class CmdHandlerBase: raise return Done # parameters should be updated already - def get_write_func(self, pname): + def get_write_func(self, pname, wfunc): """returns the write function passed to the metaclass - return None if not used. + may be overriden to return None, if not used """ - def wfunc(module, value, cmd=self, pname=pname): - cmd.write(module, {pname: value}) - return Done + if wfunc: + def new_wfunc(module, value, cmd=self, pname=pname, wfunc=wfunc): + value = wfunc(module, value) + if value is None or value is Done: + return value + cmd.write(module, {pname: value}) + return Done + else: + def new_wfunc(module, value, cmd=self, pname=pname): + cmd.write(module, {pname: value}) + return Done - return wfunc + return new_wfunc def write(self, module, valuedict, force_read=False): """write values to the module - When called from write_, valuedict contains only one item: - the parameter to be changed. - When called from initialization, valuedict may have more items. + When called from write_, valuedict contains only one item, + the single parameter to be changed. + If called directly, valuedict may have more items. """ analyze = getattr(module, 'analyze_' + self.group) + if module.writeDict: # collect other parameters to be written + valuedict = dict(valuedict) + for p in self.parameters: + if p in self.writeDict: + valuedict[p] = self.writeDict.pop(p) + elif p not in valuedict: + force_read = True if self.READ_BEFORE_WRITE or force_read: # do a read of the current hw values values = self.send_command(module) # convert them to parameters analyze(*values) if not self.READ_BEFORE_WRITE: - values = () - # create wrapper object 'new' with changed parameter 'pname' - new = ChangeWrapper(module, valuedict) + values = () # values are not expected for change_ + new = Change(module, self.parameters, valuedict) # call change_* for calculation new hw values values = getattr(module, 'change_' + self.group)(new, *values) if values is None: # this indicates that nothing has to be written @@ -273,6 +291,13 @@ class CmdHandlerBase: # send the change command and a query command analyze(*self.send_change(module, *values)) + def change(self, module, *values): + """write and read back values + + might be called from a write method, if change_ is not implemented + """ + getattr(module, 'analyze_' + self.group)(*self.send_change(module, *values)) + class CmdHandler(CmdHandlerBase): """more evolved command handler @@ -290,16 +315,19 @@ class CmdHandler(CmdHandlerBase): # the given separator - def __init__(self, group, querycmd, replyfmt): + def __init__(self, group, querycmd, replyfmt, changecmd=None): """initialize the command handler group: the handler group (used for analyze_ and change_) querycmd: the command for a query, may contain named formats for cmdargs replyfmt: the format for reading the reply with some scanf like behaviour + changecmd: the first part of the change command (without values), may be + omitted if no write happens """ super().__init__(group) self.querycmd = querycmd self.replyfmt = CmdParser(replyfmt) + self.changecmd = changecmd def parse_reply(self, reply): """return values from a raw reply""" @@ -310,11 +338,8 @@ class CmdHandler(CmdHandlerBase): return self.querycmd % {k: getattr(module, k, None) for k in self.CMDARGS} def make_change(self, module, *values): - """make a change command from a query command""" - changecmd = self.querycmd.replace('?', ' ') - if not self.querycmd.endswith('?'): - changecmd += ',' - changecmd %= {k: getattr(module, k, None) for k in self.CMDARGS} + """make a change command""" + changecmd = self.changecmd % {k: getattr(module, k, None) for k in self.CMDARGS} return changecmd + self.replyfmt.format(*values) def send_change(self, module, *values): diff --git a/secop/metaclass.py b/secop/metaclass.py index de59834..8aca3c8 100644 --- a/secop/metaclass.py +++ b/secop/metaclass.py @@ -112,12 +112,12 @@ class ModuleMeta(PropertyMeta): # skip commands for now continue rfunc = attrs.get('read_' + pname, None) - handler = pobj.handler.get_read_func(newtype, pname) if pobj.handler else None - if handler: + rfunc_handler = pobj.handler.get_read_func(newtype, pname) if pobj.handler else None + if rfunc_handler: if rfunc: raise ProgrammingError("parameter '%s' can not have a handler " "and read_%s" % (pname, pname)) - rfunc = handler + rfunc = rfunc_handler else: for base in bases: if rfunc is not None: @@ -151,17 +151,13 @@ class ModuleMeta(PropertyMeta): if not pobj.readonly: wfunc = attrs.get('write_' + pname, None) - handler = pobj.handler.get_write_func(pname) if pobj.handler else None - if handler: - if wfunc: - raise ProgrammingError("parameter '%s' can not have a handler " - "and write_%s" % (pname, pname)) - wfunc = handler - else: - for base in bases: - if wfunc is not None: - break - wfunc = getattr(base, 'write_' + pname, None) + # if a handler and write_ is present, wfunc will be called + # by the handler first + wfunc = pobj.handler.get_write_func(pname, wfunc) if pobj.handler else wfunc + for base in bases: + if wfunc is not None: + break + wfunc = getattr(base, 'write_' + pname, None) if wfunc is None or getattr(wfunc, '__wrapped__', False) is False: diff --git a/secop/modules.py b/secop/modules.py index ebe5aa8..5cb3542 100644 --- a/secop/modules.py +++ b/secop/modules.py @@ -255,7 +255,7 @@ class Module(HasProperties, metaclass=ModuleMeta): pobj = self.parameters[pname] self.DISPATCHER.announce_update_error(self, pname, pobj, exception) - def isBusy(self): + def isBusy(self, status=None): '''helper function for treating substates of BUSY correctly''' # defined even for non drivable (used for dynamic polling) return False @@ -295,21 +295,11 @@ class Module(HasProperties, metaclass=ModuleMeta): with proper error handling """ try: - pobj = self.parameters[pname] - if pobj.handler: - pnames = pobj.handler.parameters - valuedict = {n: self.writeDict.pop(n) for n in pnames if n in self.writeDict} - if valuedict: - self.log.debug('write parameters %r', valuedict) - pobj.handler.write(self, valuedict, force_read=True) - return - pobj.handler.read(self) + if pname in self.writeDict: + self.log.debug('write parameter %s', pname) + getattr(self, 'write_'+ pname)(self.writeDict.pop(pname)) else: - if pname in self.writeDict: - self.log.debug('write parameter %s', pname) - getattr(self, 'write_'+ pname)(self.writeDict.pop(pname)) - else: - getattr(self, 'read_'+ pname)() + getattr(self, 'read_'+ pname)() except SilentError as e: pass except SECoPError as e: @@ -428,9 +418,13 @@ class Drivable(Writable): 'status' : Override(datatype=TupleOf(EnumType(Status), StringType())), } - def isBusy(self): + def isBusy(self, status=None): '''helper function for treating substates of BUSY correctly''' - return 300 <= self.status[0] < 400 + return 300 <= (status or self.status)[0] < 400 + + def isDriving(self, status=None): + '''helper function (finalize is busy, not driving)''' + return 300 <= (status or self.status)[0] < 380 # improved polling: may poll faster if module is BUSY def pollParams(self, nr=0): diff --git a/secop_psi/ls370res.py b/secop_psi/ls370res.py index 9a124f8..10fc61c 100644 --- a/secop_psi/ls370res.py +++ b/secop_psi/ls370res.py @@ -36,6 +36,13 @@ class CmdHandler(secop.commandhandler.CmdHandler): CMDARGS = ['channel'] CMDSEPARATOR = ';' + def __init__(self, name, querycmd, replyfmt): + changecmd = querycmd.replace('?', ' ') + if not querycmd.endswith('?'): + changecmd += ',' + super().__init__(name, querycmd, replyfmt, changecmd) + + rdgrng = CmdHandler('rdgrng', 'RDGRNG?%(channel)d', '%d,%d,%d,%d,%d') inset = CmdHandler('inset', 'INSET?%(channel)d', '%d,%d,%d,%d,%d') filterhdl = CmdHandler('filt', 'FILTER?%(channel)d', '%d,%d,%d') diff --git a/secop_psi/ppms.py b/secop_psi/ppms.py index 0420d6b..01d37cd 100755 --- a/secop_psi/ppms.py +++ b/secop_psi/ppms.py @@ -39,7 +39,6 @@ settings and target would do a useless cycle of ramping up leads, heating switch import time import threading -import json from secop.modules import Module, Readable, Drivable, Parameter, Override,\ Communicator, Property, Attached @@ -48,6 +47,9 @@ from secop.datatypes import EnumType, FloatRange, IntRange, StringType,\ from secop.lib.enum import Enum from secop.errors import HardwareError from secop.poller import Poller +import secop.commandhandler +from secop.stringio import HasIodev +from secop.metaclass import Done try: import secop_psi.ppmswindows as ppmshw @@ -55,9 +57,15 @@ except ImportError: import secop_psi.ppmssim as ppmshw -def isDriving(status): - """moving towards target""" - return 300 <= status[0] < 390 +class CmdHandler(secop.commandhandler.CmdHandler): + CMDARGS = ['no'] + CMDSEPARATOR = None # no command chaining + READ_BEFORE_WRITE = False + + def __init__(self, name, querycmd, replyfmt): + changecmd = querycmd.split('?')[0] + ' ' + super().__init__(name, querycmd, replyfmt, changecmd) + class Main(Communicator): """general ppms dummy module""" @@ -122,82 +130,41 @@ class Main(Communicator): return data # return data as string -class PpmsMixin(Module): +class PpmsMixin(HasIodev, Module): properties = { - 'iodev': Attached('_main'), - } - parameters = { - 'settings': - Parameter('internal', export=False, poll=True, readonly=False, - default="", datatype=StringType()), + 'iodev': Attached(), } pollerClass = Poller enabled = True # default, if no parameter enable is defined - STATUS_MAP = {} # a mapping converting ppms status codes into SECoP status values - _settingnames = [] # names of the parameters in the settings command - _last_target_change = 0 + # STATUS_MAP = {} # a mapping converting ppms status codes into SECoP status values + _last_target_change = 0 # used by several modules + _last_settings = None # used by several modules slow_pollfactor = 1 def initModule(self): - self._main.register(self) + self._iodev.register(self) def startModule(self, started_callback): # no polls except on main module started_callback() - def send_cmd(self, writecmd, argdict): - self._main.do_communicate(writecmd + ' ' + - ','.join('%.7g' % argdict[key] for key in self._settingnames)) - - def get_reply(self, settings, query): - """return a dict with the values get from the reply - - if the reply has not changed, an empty dict is returned - """ - reply = self._main.do_communicate(query) - if getattr(self, settings) == reply: - return {} - setattr(self, settings, reply) - return dict(zip(self._settingnames, json.loads('[%s]' % reply))) - - def apply_reply(self, reply, pname): - """apply reply dict to the parameters - - except for reply[pname], which is returned - """ - returnvalue = getattr(self, pname) - for key, value in reply.items(): - if key == pname: - returnvalue = value - else: - setattr(self, key, value) - return returnvalue - - def make_argdict(self, pname, value): - """make a dict from the parameters self._settingnames - - but result[pname] replaced by value - """ - return {key: value if key == pname else getattr(self, key) for key in self._settingnames} - - def read_settings(self): - return self.get_settings('settings') - def read_value(self): - """not very useful, as values are updated fast enough - - note: this will update all values, and the value of this module twice - """ - self._main.read_data() + """effective polling is done by the main module""" + if not self.enabled: + return Done + if self.parameters['value'].timestamp == 0: + # make sure that the value is read at least after init + self._iodev.read_data() return self.value def read_status(self): - """not very useful, as status is updated fast enough - - note: this will update the status of all modules, and this module twice - """ - self._main.read_data() + """effective polling is done by the main module""" + if not self.enabled: + return Done + if self.parameters['value'].timestamp == 0: + # make sure that the value is read at least after init + self._iodev.read_data() return self.status def update_value_status(self, value, packed_status): @@ -214,12 +181,11 @@ class PpmsMixin(Module): self.value = value self.status = [self.Status.IDLE, ''] + class Channel(PpmsMixin, Readable): parameters = { 'value': - Override('main value of channels', poll=False, default=0), - 'status': - Override(poll=False), + Override('main value of channels', poll=True), 'enabled': Parameter('is this channel used?', readonly=False, poll=False, datatype=BoolType(), default=False), @@ -243,6 +209,7 @@ class Channel(PpmsMixin, Readable): def get_settings(self, pname): return '' + class UserChannel(Channel): parameters = { 'pollinterval': @@ -256,169 +223,86 @@ class UserChannel(Channel): class DriverChannel(Channel): + drvout = CmdHandler('drvout', 'DRVOUT? %(no)d', '%d,%g,%g') + parameters = { 'current': - Parameter('driver current', readonly=False, poll=False, - datatype=FloatRange(0., 5000., unit='uA'), default=0), + Parameter('driver current', readonly=False, handler=drvout, + datatype=FloatRange(0., 5000., unit='uA')), 'powerlimit': - Parameter('power limit', readonly=False, poll=False, - datatype=FloatRange(0., 1000., unit='uW'), default=0), + Parameter('power limit', readonly=False, handler=drvout, + datatype=FloatRange(0., 1000., unit='uW')), 'pollinterval': Override(visibility=3), } - _settingnames = ['no', 'current', 'powerlimit'] + def analyze_drvout(self, no, current, powerlimit): + if self.no != no: + raise HardwareError('DRVOUT command: channel number in reply does not match') + self.current = current + self.powerlimit = powerlimit - def get_settings(self, pname): - """read settings - - return the value for and update all other parameters - """ - reply = self.get_reply('settings', 'DRVOUT? %d' % self.no) - if reply: - if self.no != reply.pop('no'): - raise HardwareError('DRVOUT command: channel number in reply does not match') - return self.apply_reply(reply, pname) - - def put_settings(self, value, pname): - """write settings, combining = and current attributes - - and request updated settings - """ - self.send_cmd('DRVOUT', self.make_argdict(pname, value)) - return self.get_settings(pname) - - def read_current(self): - return self.get_settings('current') - - def read_powerlimit(self): - return self.get_settings('powerlimit') - - def write_settings(self): - return self.get_settings('settings') - - def write_current(self, value): - return self.put_settings(value, 'current') - - def write_powerlimit(self, value): - return self.put_settings(value, 'powerlimit') + def change_drvout(self, new): + return new.current, new.powerlimit class BridgeChannel(Channel): + bridge = CmdHandler('bridge', 'BRIDGE? %(no)d', '%d,%g,%g,%d,%d,%g') # pylint: disable=invalid-name ReadingMode = Enum('ReadingMode', standard=0, fast=1, highres=2) parameters = { + 'enabled': + Override(handler=bridge), 'excitation': - Parameter('excitation current', readonly=False, poll=False, - datatype=FloatRange(0.01, 5000., unit='uA'), default=0.01), + Parameter('excitation current', readonly=False, handler=bridge, + datatype=FloatRange(0.01, 5000., unit='uA')), 'powerlimit': - Parameter('power limit', readonly=False, poll=False, - datatype=FloatRange(0.001, 1000., unit='uW'), default=0.001), + Parameter('power limit', readonly=False, handler=bridge, + datatype=FloatRange(0.001, 1000., unit='uW')), 'dcflag': - Parameter('True when excitation is DC (else AC)', readonly=False, poll=False, - datatype=BoolType(), default=False), + Parameter('True when excitation is DC (else AC)', readonly=False, handler=bridge, + datatype=BoolType()), 'readingmode': - Parameter('reading mode', readonly=False, poll=False, - datatype=EnumType(ReadingMode), default=ReadingMode.standard), + Parameter('reading mode', readonly=False, handler=bridge, + datatype=EnumType(ReadingMode)), 'voltagelimit': - Parameter('voltage limit', readonly=False, poll=False, - datatype=FloatRange(0.0001, 100., unit='mV'), default=0.0001), + Parameter('voltage limit', readonly=False, handler=bridge, + datatype=FloatRange(0.0001, 100., unit='mV')), 'pollinterval': Override(visibility=3), } _settingnames = ['no', 'excitation', 'powerlimit', 'dcflag', 'readingmode', 'voltagelimit'] - def get_settings(self, pname): - """read settings + def analyze_bridge(self, no, excitation, powerlimit, dcflag, readingmode, voltagelimit): + if self.no != no: + raise HardwareError('DRVOUT command: channel number in reply does not match') + self.enabled = excitation != 0 and powerlimit != 0 and voltagelimit != 0 + self.excitation = excitation or self.excitation + self.powerlimit = powerlimit or self.powerlimit + self.dcflag = dcflag + self.readingmode = readingmode + self.voltagelimit = voltagelimit or self.voltagelimit - return the value for and update all other parameters - """ - reply = self.get_reply('settings', 'BRIDGE? %d' % self.no) - if reply: - if self.no != reply['no']: - raise HardwareError('BRIDGE command: channel number in reply does not match') - reply['enabled'] = 1 - if reply['excitation'] == 0: - reply['excitation'] = self.excitation - reply['enabled'] = 0 - if reply['powerlimit'] == 0: - reply['powerlimit'] = self.powerlimit - reply['enabled'] = 0 - if reply['voltagelimit'] == 0: - reply['voltagelimit'] = self.voltagelimit - reply['enabled'] = 0 - del reply['no'] - returnvalue = self.apply_reply(reply, pname) - return returnvalue - - def put_settings(self, value, pname): - """write settings, combining = and current attributes - - and request updated settings - """ - argdict = self.make_argdict(pname, value) - enabled = value if pname == 'enabled' else self.enabled - if not enabled: - argdict['excitation'] = 0 - argdict['powerlimit'] = 0 - argdict['voltagelimit'] = 0 - self.send_cmd('BRIDGE', argdict) - returnvalue = self.get_settings(pname) - return returnvalue - - def read_enabled(self): - return self.get_settings('enabled') - - def read_excitation(self): - return self.get_settings('excitation') - - def read_powerlimit(self): - return self.get_settings('powerlimit') - - def read_dcflag(self): - return self.get_settings('dcflag') - - def read_readingmode(self): - return self.get_settings('readingmode') - - def read_voltagelimit(self): - return self.get_settings('voltagelimit') - - def write_settings(self): - return self.get_settings('settings') - - def write_enabled(self, value): - return self.put_settings(value, 'enabled') - - def write_excitation(self, value): - return self.put_settings(value, 'excitation') - - def write_powerlimit(self, value): - return self.put_settings(value, 'powerlimit') - - def write_dcflag(self, value): - return self.put_settings(value, 'dcflag') - - def write_readingmode(self, value): - return self.put_settings(value, 'readingmode') - - def write_voltagelimit(self, value): - return self.put_settings(value, 'voltagelimit') + def change_bridge(self, new): + if new.enabled: + return self.no, new.excitation, new.powerlimit, new.dcflag, new.readingmode, new.voltagelimit + return self.no, 0, 0, new.dcflag, new.readingmode, 0 class Level(PpmsMixin, Readable): """helium level""" + level = CmdHandler('level', 'LEVEL?', '%g,%d') + parameters = { - 'value': Override(datatype=FloatRange(unit='%'), poll=False, default=0), - 'status': Override(poll=False), + 'value': Override(datatype=FloatRange(unit='%'), handler=level), + 'status': Override(handler=level), 'pollinterval': Override(visibility=3), } channel = 'level' - _settingnames = ['value', 'status'] def update_value_status(self, value, packed_status): """must be a no-op @@ -427,24 +311,12 @@ class Level(PpmsMixin, Readable): value and status is polled via settings """ - def get_settings(self, pname): - """read settings - - return the value for and update all other parameters - """ - reply = self.get_reply('settings', 'LEVEL?') - if reply: - if reply['status']: - reply['status'] = [self.Status.IDLE, ''] - else: - reply['status'] = [self.Status.ERROR, 'old reading'] - return self.apply_reply(reply, pname) - - def read_value(self): - return self.get_settings('value') - - def read_status(self): - return self.get_settings('status') + def analyze_level(self, level, status): + if status: + self.status = [self.Status.IDLE, ''] + else: + self.status = [self.Status.ERROR, 'old reading'] + self.value = level class Chamber(PpmsMixin, Drivable): @@ -453,6 +325,7 @@ class Chamber(PpmsMixin, Drivable): value is an Enum, which is redundant with the status text """ + chamber = CmdHandler('chamber', 'CHAMBER?', '%d') Status = Drivable.Status # pylint: disable=invalid-name Operation = Enum( @@ -481,13 +354,11 @@ class Chamber(PpmsMixin, Drivable): ) parameters = { 'value': - Override(description='chamber state', poll=False, - datatype=EnumType(StatusCode), default='unknown'), - 'status': - Override(poll=False), + Override(description='chamber state', handler=chamber, + datatype=EnumType(StatusCode)), 'target': - Override(description='chamber command', poll=True, - datatype=EnumType(Operation), default=Operation.noop), + Override(description='chamber command', handler=chamber, + datatype=EnumType(Operation)), 'pollinterval': Override(visibility=3), } @@ -513,34 +384,23 @@ class Chamber(PpmsMixin, Drivable): self.value = (packed_status >> 8) & 0xf self.status = self.STATUS_MAP[self.value] - def get_settings(self, pname): - """read settings + def analyze_chamber(self, target): + self.target = target - return the value for and update all other parameters - """ - reply = self.get_reply('settings', 'CHAMBER?') - return self.apply_reply(reply, pname) - - def put_settings(self, value, pname): + def change_chamber(self, new): """write settings, combining = and current attributes and request updated settings """ - self.send_cmd('CHAMBER', self.make_argdict(pname, value)) - return self.get_settings(pname) - - def read_target(self): - return self.get_settings('target') - - def write_target(self, value): - if value == self.Operation.noop: - return value - return self.put_settings(value, 'target') + if new.target == self.Operation.noop: + return None + return (new.target,) class Temp(PpmsMixin, Drivable): """temperature""" + temp = CmdHandler('temp', 'TEMP?', '%g,%g,%d') Status = Enum(Drivable.Status, RAMPING = 370, STABILIZING = 380, @@ -549,17 +409,17 @@ class Temp(PpmsMixin, Drivable): ApproachMode = Enum('ApproachMode', fast_settle=0, no_overshoot=1) parameters = { 'value': - Override(datatype=FloatRange(unit='K'), poll=False, default=0), + Override(datatype=FloatRange(unit='K'), poll=True), 'status': - Override(poll=False, datatype=StatusType(Status)), + Override(datatype=StatusType(Status), poll=True), 'target': - Override(datatype=FloatRange(1.7, 402.0, unit='K'), default=295, poll=False), + Override(datatype=FloatRange(1.7, 402.0, unit='K'), handler=temp), 'ramp': - Parameter('ramping speed', readonly=False, poll=False, - datatype=FloatRange(0, 20, unit='K/min'), default=0.1), + Parameter('ramping speed', readonly=False, handler=temp, + datatype=FloatRange(0, 20, unit='K/min')), 'approachmode': - Parameter('how to approach target!', readonly=False, poll=False, - datatype=EnumType(ApproachMode), default=0), + Parameter('how to approach target!', readonly=False, handler=temp, + datatype=EnumType(ApproachMode)), 'pollinterval': Override(visibility=3), 'timeout': @@ -621,7 +481,7 @@ class Temp(PpmsMixin, Drivable): self.status = [self.Status.IDLE, 'stopped(%s)' % status[1]] return if self._last_change: # there was a change, which is not yet confirmed by hw - if isDriving(status): + if self.isDriving(status): if now > self._last_change + 15 or status != self._status_before_change: self._last_change = 0 self.log.debug('time needed to change to busy: %.3g', now - self._last_change) @@ -632,71 +492,50 @@ class Temp(PpmsMixin, Drivable): status = [self.Status.WARN, 'temperature status (%r) does not change to BUSY' % status] if self._expected_target: # handle timeout - if isDriving(status): + if self.isDriving(status): if now > self._expected_target + self.timeout: - self.status = [self.Status.WARN, 'timeout while %s' % status[1]] - return + status = [self.Status.WARN, 'timeout while %s' % status[1]] else: self._expected_target = 0 self.status = status - def get_settings(self, pname): - """read settings + def analyze_temp(self, target, ramp, approachmode): + if (target, ramp, approachmode) != self._last_settings: + # we update parameters only on change, as 'approachmode' + # is not always sent to the hardware + self._last_settings = target, ramp, approachmode + self.target = target + self.ramp =ramp + self.approachmode = approachmode - return the value for and update all other parameters - """ - return self.apply_reply(self.get_reply('settings', 'TEMP?'), pname) + def change_temp(self, new): + self.calc_expected(new.target, self.ramp) + return new.target, new.ramp, new.approachmode - def put_settings(self, value, pname): - """write settings, combining = and current attributes + def write_target(self, target): + self._stopped = False + if abs(self.target - self.value) < 2e-5 and target == self.target: + return None + self._status_before_change = self.status + self.status = [self.Status.BUSY, 'changed_target'] + self._last_change = time.time() + return target - and request updated settings - """ - self.send_cmd('TEMP', self.make_argdict(pname, value)) - return self.get_settings(pname) + def write_approachmode(self, value): + if self.isDriving(): + return value + return None # change_temp will not be called, as this would trigger an unnecessary T change - def read_target(self): - return self.get_settings('target') - - def read_ramp(self): - return self.get_settings('ramp') - - def read_approachmode(self): - return self.get_settings('approachmode') - - def write_settings(self): - return self.get_settings('settings') + def write_ramp(self, value): + if self.isDriving(): + return value + return None # change_temp will not be called, as this would trigger an unnecessary T change def calc_expected(self, target, ramp): self._expected_target = time.time() + abs(target - self.value) * 60.0 / max(0.1, ramp) - def write_target(self, target): - self._stopped = False - if abs(self.value - target) < 2e-5 and target == self.target: - return target # no action needed - self._status_before_change = self.status - self.status = [self.Status.BUSY, 'changed_target'] - self._last_change = time.time() - newtarget = self.put_settings(target, 'target') - self.calc_expected(target, self.ramp) - return newtarget - - def write_ramp(self, value): - if not isDriving(self.status): - # do not yet write settings, as this may change the status to busy - return value - if time.time() < self._expected_target: # recalc expected target - self.calc_expected(self.target, value) - return self.put_settings(value, 'ramp') - - def write_approachmode(self, value): - if not isDriving(self.status): - # do not yet write settings, as this may change the status to busy - return value - return self.put_settings(value, 'approachmode') - def do_stop(self): - if not isDriving(self.status): + if self.isDriving(): return if self.status[0] == self.Status.STABILIZING: # we are already near target @@ -712,6 +551,7 @@ class Temp(PpmsMixin, Drivable): class Field(PpmsMixin, Drivable): """magnetic field""" + field = CmdHandler('field', 'FIELD?', '%g,%g,%d,%d') Status = Enum(Drivable.Status, PREPARED = 150, PREPARING = 340, @@ -724,20 +564,20 @@ class Field(PpmsMixin, Drivable): parameters = { 'value': - Override(datatype=FloatRange(unit='T'), poll=False, default=0), + Override(datatype=FloatRange(unit='T'), poll=True), 'status': - Override(poll=False, datatype=StatusType(Status)), + Override(datatype=StatusType(Status), poll=True), 'target': - Override(datatype=FloatRange(-15,15,unit='T'), poll=False), + Override(datatype=FloatRange(-15,15,unit='T'), handler=field), 'ramp': - Parameter('ramping speed', readonly=False, poll=False, - datatype=FloatRange(0.064, 1.19, unit='T/min'), default=0.064), + Parameter('ramping speed', readonly=False, handler=field, + datatype=FloatRange(0.064, 1.19, unit='T/min')), 'approachmode': - Parameter('how to approach target', readonly=False, poll=False, - datatype=EnumType(ApproachMode), default=0), + Parameter('how to approach target', readonly=False, handler=field, + datatype=EnumType(ApproachMode)), 'persistentmode': - Parameter('what to do after changing field', readonly=False, poll=False, - datatype=EnumType(PersistentMode), default=0), + Parameter('what to do after changing field', readonly=False, handler=field, + datatype=EnumType(PersistentMode)), 'pollinterval': Override(visibility=3), } @@ -756,7 +596,6 @@ class Field(PpmsMixin, Drivable): } channel = 'field' - _settingnames = ['target', 'ramp', 'approachmode', 'persistentmode'] _stopped = False _last_target = 0 _last_change= 0 # means no target change is pending @@ -784,7 +623,7 @@ class Field(PpmsMixin, Drivable): status = [self.Status.PREPARING, 'ramping leads'] else: status = [self.Status.WARN, 'timeout when ramping leads'] - elif isDriving(status): + elif self.isDriving(status): if now > self._last_change + 5 or status != self._status_before_change: self._last_change = 0 self.log.debug('time needed to change to busy: %.3g', now - self._last_change) @@ -793,82 +632,34 @@ class Field(PpmsMixin, Drivable): status = [self.Status.BUSY, 'changed target while %s' % status[1]] else: status = [self.Status.WARN, 'field status (%r) does not change to BUSY' % status] - - self.status = status - def _start(self): - """common code for change target and change persistentmode""" - self._last_change = time.time() - self._status_before_change = list(self.status) + def analyze_field(self, target, ramp, approachmode, persistentmode): + 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 + self._last_settings = target, ramp, approachmode, persistentmode + self.target = target * 1e-4 + self.ramp = ramp * 6e-3 + self.approachmode = approachmode + self.persistentmode = persistentmode - def get_settings(self, pname): - """read settings - - return the value for and update all other parameters - """ - reply = self.get_reply('settings', 'FIELD?') - if reply: - reply['target'] *= 1e-4 - reply['ramp'] *= 6e-3 - return self.apply_reply(reply, pname) - - def put_settings(self, value, pname): - """write settings, combining = and current attributes - - and request updated settings - """ - argdict = self.make_argdict(pname, value) - argdict['target'] *= 1e+4 - argdict['ramp'] /= 6e-3 - self.send_cmd('FIELD', argdict) - return self.get_settings(pname) - - def read_target(self): - return self.get_settings('target') - - def read_ramp(self): - return self.get_settings('ramp') - - def read_approachmode(self): - return self.get_settings('approachmode') - - def read_persistentmode(self): - return self.get_settings('persistentmode') - - def write_settings(self): - return self.get_settings('settings') - - def write_target(self, target): - self._last_target = self.target # save for stop command - self._stopped = False - if abs(self.value - target) < 2e-5 and target == self.target: - return target # no action needed - self._start() - result = self.put_settings(target, 'target') - self._main.read_data() # update status - return result - - def write_ramp(self, value): - if not isDriving(self.status): - # do not yet write settings, as this will trigger a ramp up of leads current - return value - return self.put_settings(value, 'ramp') - - def write_approachmode(self, value): - if not isDriving(self.status): - # do not yet write settings, as this will trigger a ramp up of leads current - return value - return self.put_settings(value, 'approachmode') - - def write_persistentmode(self, value): - if self.persistentmode == value: - return value # no action needed - self._start() - return self.put_settings(value, 'persistentmode') + def change_field(self, new): + if 'target' in new or 'persistentmode' in new: + # changed target or persistentmode + if 'target' in new: + self._last_target = self.target # save for stop command + self._stopped = False + self._last_change = time.time() + self._status_before_change = list(self.status) + else: + # changed ramp or approachmode + if not self.isDriving(): + return None # nothing to be written, as this would trigger a ramp up of leads current + return new.target * 1e+4, new.ramp / 6e-3, new.approachmode, new.persistentmode def do_stop(self): - if not isDriving(self.status): + if not self.isDriving(): return self.status = [self.Status.IDLE, '_stopped'] self._stopped = True @@ -884,20 +675,19 @@ class Field(PpmsMixin, Drivable): class Position(PpmsMixin, Drivable): """rotator position""" + move = CmdHandler('move', 'MOVE?', '%g,%g,%g') Status = Drivable.Status parameters = { 'value': - Override(datatype=FloatRange(unit='deg'), poll=False, default=0), - 'status': - Override(poll=False), + Override(datatype=FloatRange(unit='deg'), poll=True), 'target': - Override(datatype=FloatRange(-720., 720., unit='deg'), default=0., poll=False), + Override(datatype=FloatRange(-720., 720., unit='deg'), handler=move), 'enabled': Parameter('is this channel used?', readonly=False, poll=False, datatype=BoolType(), default=True), 'speed': - Parameter('motor speed', readonly=False, poll=False, - datatype=FloatRange(0.8, 12, unit='deg/sec'), default=12.0), + Parameter('motor speed', readonly=False, handler=move, + datatype=FloatRange(0.8, 12, unit='deg/sec')), 'pollinterval': Override(visibility=3), } @@ -915,7 +705,6 @@ class Position(PpmsMixin, Drivable): _stopped = False _last_target = 0 _last_change = 0 # means no target change is pending - mode = 0 # always use normal mode def update_value_status(self, value, packed_status): """update value and status""" @@ -935,7 +724,7 @@ class Position(PpmsMixin, Drivable): status = [self.Status.IDLE, 'stopped(%s)' % status[1]] if self._last_change: # there was a change, which is not yet confirmed by hw now = time.time() - if isDriving(status): + if self.isDriving(): if now > self._last_change + 15 or status != self._status_before_change: self._last_change = 0 self.log.debug('time needed to change to busy: %.3g', now - self._last_change) @@ -946,50 +735,32 @@ class Position(PpmsMixin, Drivable): status = [self.Status.WARN, 'temperature status (%r) does not change to BUSY' % status] self.status = status - def get_settings(self, pname): - """read settings + 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 + self._last_settings = target, speed + self.target = target + self.speed = (15 - speed) * 0.8 - return the value for and update all other parameters - """ - reply = self.get_reply('settings', 'MOVE?') - if reply: - reply['speed'] = (15 - reply['speed']) * 0.8 - reply.pop('mode', None) - return self.apply_reply(reply, pname) + def change_move(self, new): + speed = int(round(min(14, max(0, 15 - new.speed / 0.8)), 0)) + return new.target, 0, speed - def put_settings(self, value, pname): - """write settings, combining = and current attributes - - and request updated settings - """ - argdict = self.make_argdict(pname, value) - argdict['speed'] = int(round(min(14, max(0, 15 - argdict['speed'] / 0.8)), 0)) - self.send_cmd('MOVE', argdict) - return self.get_settings(pname) - - def read_target(self): - return self.get_settings('target') - - def read_speed(self): - return self.get_settings('speed') - - def write_settings(self): - return self.get_settings('settings') - - def write_target(self, value): + def write_target(self, target): self._last_target = self.target # save for stop command self._stopped = False self._last_change = 0 self._status_before_change = self.status - return self.put_settings(value, 'target') + return target def write_speed(self, value): - if not isDriving(self.status): + if self.isDriving(): return value - return self.put_settings(value, 'speed') + return None # change_move not called: as this would trigger an unnecessary move def do_stop(self): - if not isDriving(self.status): + if not self.isDriving(): return self.status = [self.Status.BUSY, '_stopped'] self._stopped = True diff --git a/test/test_commandhandler.py b/test/test_commandhandler.py index 4206795..87e5881 100644 --- a/test/test_commandhandler.py +++ b/test/test_commandhandler.py @@ -96,6 +96,12 @@ def test_CmdHandler(): CMDARGS = ['channel', 'loop'] CMDSEPARATOR ='|' + def __init__(self, name, querycmd, replyfmt): + changecmd = querycmd.replace('?', ' ') + if not querycmd.endswith('?'): + changecmd += ',' + super().__init__(name, querycmd, replyfmt, changecmd) + group1 = Hdl('group1', 'SIMPLE?', '%g') group2 = Hdl('group2', 'CMD?%(channel)d', '%g,%s,%d')