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 <bjoern_pedersen@frm2.tum.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
This commit is contained in:
zolliker 2019-12-13 16:28:22 +01:00
parent cb4874331b
commit fcad78a682
6 changed files with 275 additions and 476 deletions

View File

@ -136,22 +136,26 @@ class CmdParser:
return [c(v) for c, v in zip(self.casts, match.groups())] return [c(v) for c, v in zip(self.casts, match.groups())]
class ChangeWrapper: class Change:
"""Wrapper around a module """contains new values for the call to change_<group>
A ChangeWrapper instance is used as the 'new' argument for the change_<group> message. A Change instance is used as the 'new' argument for the change_<group> method.
new.<parameter> is either the new, changed value or the old value from the module. new.<parameter> is either the new, changed value or the old value from the module.
In addition '<parameter>' indicates, whether <parameter> is to be changed. In addition '<parameter>' in new indicates, whether <parameter> is to be changed.
setting new.<parameter> does not yet set the value on the module. new.<parameter> can not be changed
""" """
def __init__(self, module, valuedict): def __init__(self, module, parameters, valuedict):
self._module = module self.__dict__.update(valuedict, _module=module, _parameters=parameters)
for pname, value in valuedict.items():
setattr(self, pname, value)
def __getattr__(self, key): def __getattr__(self, pname):
"""get current values from _module for unchanged parameters""" """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): def __contains__(self, pname):
"""check whether a specific parameter is to be changed""" """check whether a specific parameter is to be changed"""
@ -237,35 +241,49 @@ class CmdHandlerBase:
raise raise
return Done # parameters should be updated already 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 """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): if wfunc:
cmd.write(module, {pname: value}) def new_wfunc(module, value, cmd=self, pname=pname, wfunc=wfunc):
return Done 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): def write(self, module, valuedict, force_read=False):
"""write values to the module """write values to the module
When called from write_<param>, valuedict contains only one item: When called from write_<param>, valuedict contains only one item,
the parameter to be changed. the single parameter to be changed.
When called from initialization, valuedict may have more items. If called directly, valuedict may have more items.
""" """
analyze = getattr(module, 'analyze_' + self.group) 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: if self.READ_BEFORE_WRITE or force_read:
# do a read of the current hw values # do a read of the current hw values
values = self.send_command(module) values = self.send_command(module)
# convert them to parameters # convert them to parameters
analyze(*values) analyze(*values)
if not self.READ_BEFORE_WRITE: if not self.READ_BEFORE_WRITE:
values = () values = () # values are not expected for change_<group>
# create wrapper object 'new' with changed parameter 'pname' new = Change(module, self.parameters, valuedict)
new = ChangeWrapper(module, valuedict)
# call change_* for calculation new hw values # call change_* for calculation new hw values
values = getattr(module, 'change_' + self.group)(new, *values) values = getattr(module, 'change_' + self.group)(new, *values)
if values is None: # this indicates that nothing has to be written 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 # send the change command and a query command
analyze(*self.send_change(module, *values)) 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_<group> is not implemented
"""
getattr(module, 'analyze_' + self.group)(*self.send_change(module, *values))
class CmdHandler(CmdHandlerBase): class CmdHandler(CmdHandlerBase):
"""more evolved command handler """more evolved command handler
@ -290,16 +315,19 @@ class CmdHandler(CmdHandlerBase):
# the given separator # the given separator
def __init__(self, group, querycmd, replyfmt): def __init__(self, group, querycmd, replyfmt, changecmd=None):
"""initialize the command handler """initialize the command handler
group: the handler group (used for analyze_<group> and change_<group>) group: the handler group (used for analyze_<group> and change_<group>)
querycmd: the command for a query, may contain named formats for cmdargs querycmd: the command for a query, may contain named formats for cmdargs
replyfmt: the format for reading the reply with some scanf like behaviour 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) super().__init__(group)
self.querycmd = querycmd self.querycmd = querycmd
self.replyfmt = CmdParser(replyfmt) self.replyfmt = CmdParser(replyfmt)
self.changecmd = changecmd
def parse_reply(self, reply): def parse_reply(self, reply):
"""return values from a raw 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} return self.querycmd % {k: getattr(module, k, None) for k in self.CMDARGS}
def make_change(self, module, *values): def make_change(self, module, *values):
"""make a change command from a query command""" """make a change command"""
changecmd = self.querycmd.replace('?', ' ') changecmd = self.changecmd % {k: getattr(module, k, None) for k in self.CMDARGS}
if not self.querycmd.endswith('?'):
changecmd += ','
changecmd %= {k: getattr(module, k, None) for k in self.CMDARGS}
return changecmd + self.replyfmt.format(*values) return changecmd + self.replyfmt.format(*values)
def send_change(self, module, *values): def send_change(self, module, *values):

View File

@ -112,12 +112,12 @@ class ModuleMeta(PropertyMeta):
# skip commands for now # skip commands for now
continue continue
rfunc = attrs.get('read_' + pname, None) rfunc = attrs.get('read_' + pname, None)
handler = pobj.handler.get_read_func(newtype, pname) if pobj.handler else None rfunc_handler = pobj.handler.get_read_func(newtype, pname) if pobj.handler else None
if handler: if rfunc_handler:
if rfunc: if rfunc:
raise ProgrammingError("parameter '%s' can not have a handler " raise ProgrammingError("parameter '%s' can not have a handler "
"and read_%s" % (pname, pname)) "and read_%s" % (pname, pname))
rfunc = handler rfunc = rfunc_handler
else: else:
for base in bases: for base in bases:
if rfunc is not None: if rfunc is not None:
@ -151,17 +151,13 @@ class ModuleMeta(PropertyMeta):
if not pobj.readonly: if not pobj.readonly:
wfunc = attrs.get('write_' + pname, None) wfunc = attrs.get('write_' + pname, None)
handler = pobj.handler.get_write_func(pname) if pobj.handler else None # if a handler and write_<param> is present, wfunc will be called
if handler: # by the handler first
if wfunc: wfunc = pobj.handler.get_write_func(pname, wfunc) if pobj.handler else wfunc
raise ProgrammingError("parameter '%s' can not have a handler " for base in bases:
"and write_%s" % (pname, pname)) if wfunc is not None:
wfunc = handler break
else: wfunc = getattr(base, 'write_' + pname, None)
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: if wfunc is None or getattr(wfunc, '__wrapped__', False) is False:

View File

@ -255,7 +255,7 @@ class Module(HasProperties, metaclass=ModuleMeta):
pobj = self.parameters[pname] pobj = self.parameters[pname]
self.DISPATCHER.announce_update_error(self, pname, pobj, exception) 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''' '''helper function for treating substates of BUSY correctly'''
# defined even for non drivable (used for dynamic polling) # defined even for non drivable (used for dynamic polling)
return False return False
@ -295,21 +295,11 @@ class Module(HasProperties, metaclass=ModuleMeta):
with proper error handling with proper error handling
""" """
try: try:
pobj = self.parameters[pname] if pname in self.writeDict:
if pobj.handler: self.log.debug('write parameter %s', pname)
pnames = pobj.handler.parameters getattr(self, 'write_'+ pname)(self.writeDict.pop(pname))
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)
else: else:
if pname in self.writeDict: getattr(self, 'read_'+ pname)()
self.log.debug('write parameter %s', pname)
getattr(self, 'write_'+ pname)(self.writeDict.pop(pname))
else:
getattr(self, 'read_'+ pname)()
except SilentError as e: except SilentError as e:
pass pass
except SECoPError as e: except SECoPError as e:
@ -428,9 +418,13 @@ class Drivable(Writable):
'status' : Override(datatype=TupleOf(EnumType(Status), StringType())), 'status' : Override(datatype=TupleOf(EnumType(Status), StringType())),
} }
def isBusy(self): def isBusy(self, status=None):
'''helper function for treating substates of BUSY correctly''' '''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 # improved polling: may poll faster if module is BUSY
def pollParams(self, nr=0): def pollParams(self, nr=0):

View File

@ -36,6 +36,13 @@ class CmdHandler(secop.commandhandler.CmdHandler):
CMDARGS = ['channel'] CMDARGS = ['channel']
CMDSEPARATOR = ';' 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') rdgrng = CmdHandler('rdgrng', 'RDGRNG?%(channel)d', '%d,%d,%d,%d,%d')
inset = CmdHandler('inset', 'INSET?%(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') filterhdl = CmdHandler('filt', 'FILTER?%(channel)d', '%d,%d,%d')

View File

@ -39,7 +39,6 @@ settings and target would do a useless cycle of ramping up leads, heating switch
import time import time
import threading import threading
import json
from secop.modules import Module, Readable, Drivable, Parameter, Override,\ from secop.modules import Module, Readable, Drivable, Parameter, Override,\
Communicator, Property, Attached Communicator, Property, Attached
@ -48,6 +47,9 @@ from secop.datatypes import EnumType, FloatRange, IntRange, StringType,\
from secop.lib.enum import Enum from secop.lib.enum import Enum
from secop.errors import HardwareError from secop.errors import HardwareError
from secop.poller import Poller from secop.poller import Poller
import secop.commandhandler
from secop.stringio import HasIodev
from secop.metaclass import Done
try: try:
import secop_psi.ppmswindows as ppmshw import secop_psi.ppmswindows as ppmshw
@ -55,9 +57,15 @@ except ImportError:
import secop_psi.ppmssim as ppmshw import secop_psi.ppmssim as ppmshw
def isDriving(status): class CmdHandler(secop.commandhandler.CmdHandler):
"""moving towards target""" CMDARGS = ['no']
return 300 <= status[0] < 390 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): class Main(Communicator):
"""general ppms dummy module""" """general ppms dummy module"""
@ -122,82 +130,41 @@ class Main(Communicator):
return data # return data as string return data # return data as string
class PpmsMixin(Module): class PpmsMixin(HasIodev, Module):
properties = { properties = {
'iodev': Attached('_main'), 'iodev': Attached(),
}
parameters = {
'settings':
Parameter('internal', export=False, poll=True, readonly=False,
default="", datatype=StringType()),
} }
pollerClass = Poller pollerClass = Poller
enabled = True # default, if no parameter enable is defined enabled = True # default, if no parameter enable is defined
STATUS_MAP = {} # a mapping converting ppms status codes into SECoP status values # 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 # used by several modules
_last_target_change = 0 _last_settings = None # used by several modules
slow_pollfactor = 1 slow_pollfactor = 1
def initModule(self): def initModule(self):
self._main.register(self) self._iodev.register(self)
def startModule(self, started_callback): def startModule(self, started_callback):
# no polls except on main module # no polls except on main module
started_callback() 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): def read_value(self):
"""not very useful, as values are updated fast enough """effective polling is done by the main module"""
if not self.enabled:
note: this will update all values, and the value of this module twice return Done
""" if self.parameters['value'].timestamp == 0:
self._main.read_data() # make sure that the value is read at least after init
self._iodev.read_data()
return self.value return self.value
def read_status(self): def read_status(self):
"""not very useful, as status is updated fast enough """effective polling is done by the main module"""
if not self.enabled:
note: this will update the status of all modules, and this module twice return Done
""" if self.parameters['value'].timestamp == 0:
self._main.read_data() # make sure that the value is read at least after init
self._iodev.read_data()
return self.status return self.status
def update_value_status(self, value, packed_status): def update_value_status(self, value, packed_status):
@ -214,12 +181,11 @@ class PpmsMixin(Module):
self.value = value self.value = value
self.status = [self.Status.IDLE, ''] self.status = [self.Status.IDLE, '']
class Channel(PpmsMixin, Readable): class Channel(PpmsMixin, Readable):
parameters = { parameters = {
'value': 'value':
Override('main value of channels', poll=False, default=0), Override('main value of channels', poll=True),
'status':
Override(poll=False),
'enabled': 'enabled':
Parameter('is this channel used?', readonly=False, poll=False, Parameter('is this channel used?', readonly=False, poll=False,
datatype=BoolType(), default=False), datatype=BoolType(), default=False),
@ -243,6 +209,7 @@ class Channel(PpmsMixin, Readable):
def get_settings(self, pname): def get_settings(self, pname):
return '' return ''
class UserChannel(Channel): class UserChannel(Channel):
parameters = { parameters = {
'pollinterval': 'pollinterval':
@ -256,169 +223,86 @@ class UserChannel(Channel):
class DriverChannel(Channel): class DriverChannel(Channel):
drvout = CmdHandler('drvout', 'DRVOUT? %(no)d', '%d,%g,%g')
parameters = { parameters = {
'current': 'current':
Parameter('driver current', readonly=False, poll=False, Parameter('driver current', readonly=False, handler=drvout,
datatype=FloatRange(0., 5000., unit='uA'), default=0), datatype=FloatRange(0., 5000., unit='uA')),
'powerlimit': 'powerlimit':
Parameter('power limit', readonly=False, poll=False, Parameter('power limit', readonly=False, handler=drvout,
datatype=FloatRange(0., 1000., unit='uW'), default=0), datatype=FloatRange(0., 1000., unit='uW')),
'pollinterval': 'pollinterval':
Override(visibility=3), 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): def change_drvout(self, new):
"""read settings return new.current, new.powerlimit
return the value for <pname> 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 <pname>=<value> 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')
class BridgeChannel(Channel): class BridgeChannel(Channel):
bridge = CmdHandler('bridge', 'BRIDGE? %(no)d', '%d,%g,%g,%d,%d,%g')
# pylint: disable=invalid-name # pylint: disable=invalid-name
ReadingMode = Enum('ReadingMode', standard=0, fast=1, highres=2) ReadingMode = Enum('ReadingMode', standard=0, fast=1, highres=2)
parameters = { parameters = {
'enabled':
Override(handler=bridge),
'excitation': 'excitation':
Parameter('excitation current', readonly=False, poll=False, Parameter('excitation current', readonly=False, handler=bridge,
datatype=FloatRange(0.01, 5000., unit='uA'), default=0.01), datatype=FloatRange(0.01, 5000., unit='uA')),
'powerlimit': 'powerlimit':
Parameter('power limit', readonly=False, poll=False, Parameter('power limit', readonly=False, handler=bridge,
datatype=FloatRange(0.001, 1000., unit='uW'), default=0.001), datatype=FloatRange(0.001, 1000., unit='uW')),
'dcflag': 'dcflag':
Parameter('True when excitation is DC (else AC)', readonly=False, poll=False, Parameter('True when excitation is DC (else AC)', readonly=False, handler=bridge,
datatype=BoolType(), default=False), datatype=BoolType()),
'readingmode': 'readingmode':
Parameter('reading mode', readonly=False, poll=False, Parameter('reading mode', readonly=False, handler=bridge,
datatype=EnumType(ReadingMode), default=ReadingMode.standard), datatype=EnumType(ReadingMode)),
'voltagelimit': 'voltagelimit':
Parameter('voltage limit', readonly=False, poll=False, Parameter('voltage limit', readonly=False, handler=bridge,
datatype=FloatRange(0.0001, 100., unit='mV'), default=0.0001), datatype=FloatRange(0.0001, 100., unit='mV')),
'pollinterval': 'pollinterval':
Override(visibility=3), Override(visibility=3),
} }
_settingnames = ['no', 'excitation', 'powerlimit', 'dcflag', 'readingmode', 'voltagelimit'] _settingnames = ['no', 'excitation', 'powerlimit', 'dcflag', 'readingmode', 'voltagelimit']
def get_settings(self, pname): def analyze_bridge(self, no, excitation, powerlimit, dcflag, readingmode, voltagelimit):
"""read settings 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 <pname> and update all other parameters def change_bridge(self, new):
""" if new.enabled:
reply = self.get_reply('settings', 'BRIDGE? %d' % self.no) return self.no, new.excitation, new.powerlimit, new.dcflag, new.readingmode, new.voltagelimit
if reply: return self.no, 0, 0, new.dcflag, new.readingmode, 0
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 <pname>=<value> 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')
class Level(PpmsMixin, Readable): class Level(PpmsMixin, Readable):
"""helium level""" """helium level"""
level = CmdHandler('level', 'LEVEL?', '%g,%d')
parameters = { parameters = {
'value': Override(datatype=FloatRange(unit='%'), poll=False, default=0), 'value': Override(datatype=FloatRange(unit='%'), handler=level),
'status': Override(poll=False), 'status': Override(handler=level),
'pollinterval': 'pollinterval':
Override(visibility=3), Override(visibility=3),
} }
channel = 'level' channel = 'level'
_settingnames = ['value', 'status']
def update_value_status(self, value, packed_status): def update_value_status(self, value, packed_status):
"""must be a no-op """must be a no-op
@ -427,24 +311,12 @@ class Level(PpmsMixin, Readable):
value and status is polled via settings value and status is polled via settings
""" """
def get_settings(self, pname): def analyze_level(self, level, status):
"""read settings if status:
self.status = [self.Status.IDLE, '']
return the value for <pname> and update all other parameters else:
""" self.status = [self.Status.ERROR, 'old reading']
reply = self.get_reply('settings', 'LEVEL?') self.value = 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')
class Chamber(PpmsMixin, Drivable): class Chamber(PpmsMixin, Drivable):
@ -453,6 +325,7 @@ class Chamber(PpmsMixin, Drivable):
value is an Enum, which is redundant with the status text value is an Enum, which is redundant with the status text
""" """
chamber = CmdHandler('chamber', 'CHAMBER?', '%d')
Status = Drivable.Status Status = Drivable.Status
# pylint: disable=invalid-name # pylint: disable=invalid-name
Operation = Enum( Operation = Enum(
@ -481,13 +354,11 @@ class Chamber(PpmsMixin, Drivable):
) )
parameters = { parameters = {
'value': 'value':
Override(description='chamber state', poll=False, Override(description='chamber state', handler=chamber,
datatype=EnumType(StatusCode), default='unknown'), datatype=EnumType(StatusCode)),
'status':
Override(poll=False),
'target': 'target':
Override(description='chamber command', poll=True, Override(description='chamber command', handler=chamber,
datatype=EnumType(Operation), default=Operation.noop), datatype=EnumType(Operation)),
'pollinterval': 'pollinterval':
Override(visibility=3), Override(visibility=3),
} }
@ -513,34 +384,23 @@ class Chamber(PpmsMixin, Drivable):
self.value = (packed_status >> 8) & 0xf self.value = (packed_status >> 8) & 0xf
self.status = self.STATUS_MAP[self.value] self.status = self.STATUS_MAP[self.value]
def get_settings(self, pname): def analyze_chamber(self, target):
"""read settings self.target = target
return the value for <pname> and update all other parameters def change_chamber(self, new):
"""
reply = self.get_reply('settings', 'CHAMBER?')
return self.apply_reply(reply, pname)
def put_settings(self, value, pname):
"""write settings, combining <pname>=<value> and current attributes """write settings, combining <pname>=<value> and current attributes
and request updated settings and request updated settings
""" """
self.send_cmd('CHAMBER', self.make_argdict(pname, value)) if new.target == self.Operation.noop:
return self.get_settings(pname) return None
return (new.target,)
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')
class Temp(PpmsMixin, Drivable): class Temp(PpmsMixin, Drivable):
"""temperature""" """temperature"""
temp = CmdHandler('temp', 'TEMP?', '%g,%g,%d')
Status = Enum(Drivable.Status, Status = Enum(Drivable.Status,
RAMPING = 370, RAMPING = 370,
STABILIZING = 380, STABILIZING = 380,
@ -549,17 +409,17 @@ class Temp(PpmsMixin, Drivable):
ApproachMode = Enum('ApproachMode', fast_settle=0, no_overshoot=1) ApproachMode = Enum('ApproachMode', fast_settle=0, no_overshoot=1)
parameters = { parameters = {
'value': 'value':
Override(datatype=FloatRange(unit='K'), poll=False, default=0), Override(datatype=FloatRange(unit='K'), poll=True),
'status': 'status':
Override(poll=False, datatype=StatusType(Status)), Override(datatype=StatusType(Status), poll=True),
'target': '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': 'ramp':
Parameter('ramping speed', readonly=False, poll=False, Parameter('ramping speed', readonly=False, handler=temp,
datatype=FloatRange(0, 20, unit='K/min'), default=0.1), datatype=FloatRange(0, 20, unit='K/min')),
'approachmode': 'approachmode':
Parameter('how to approach target!', readonly=False, poll=False, Parameter('how to approach target!', readonly=False, handler=temp,
datatype=EnumType(ApproachMode), default=0), datatype=EnumType(ApproachMode)),
'pollinterval': 'pollinterval':
Override(visibility=3), Override(visibility=3),
'timeout': 'timeout':
@ -621,7 +481,7 @@ class Temp(PpmsMixin, Drivable):
self.status = [self.Status.IDLE, 'stopped(%s)' % status[1]] self.status = [self.Status.IDLE, 'stopped(%s)' % status[1]]
return return
if self._last_change: # there was a change, which is not yet confirmed by hw 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: if now > self._last_change + 15 or status != self._status_before_change:
self._last_change = 0 self._last_change = 0
self.log.debug('time needed to change to busy: %.3g', now - self._last_change) 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] status = [self.Status.WARN, 'temperature status (%r) does not change to BUSY' % status]
if self._expected_target: if self._expected_target:
# handle timeout # handle timeout
if isDriving(status): if self.isDriving(status):
if now > self._expected_target + self.timeout: if now > self._expected_target + self.timeout:
self.status = [self.Status.WARN, 'timeout while %s' % status[1]] status = [self.Status.WARN, 'timeout while %s' % status[1]]
return
else: else:
self._expected_target = 0 self._expected_target = 0
self.status = status self.status = status
def get_settings(self, pname): def analyze_temp(self, target, ramp, approachmode):
"""read settings 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 <pname> and update all other parameters def change_temp(self, new):
""" self.calc_expected(new.target, self.ramp)
return self.apply_reply(self.get_reply('settings', 'TEMP?'), pname) return new.target, new.ramp, new.approachmode
def put_settings(self, value, pname): def write_target(self, target):
"""write settings, combining <pname>=<value> and current attributes 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 def write_approachmode(self, value):
""" if self.isDriving():
self.send_cmd('TEMP', self.make_argdict(pname, value)) return value
return self.get_settings(pname) return None # change_temp will not be called, as this would trigger an unnecessary T change
def read_target(self): def write_ramp(self, value):
return self.get_settings('target') if self.isDriving():
return value
def read_ramp(self): return None # change_temp will not be called, as this would trigger an unnecessary T change
return self.get_settings('ramp')
def read_approachmode(self):
return self.get_settings('approachmode')
def write_settings(self):
return self.get_settings('settings')
def calc_expected(self, target, ramp): def calc_expected(self, target, ramp):
self._expected_target = time.time() + abs(target - self.value) * 60.0 / max(0.1, 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): def do_stop(self):
if not isDriving(self.status): if self.isDriving():
return return
if self.status[0] == self.Status.STABILIZING: if self.status[0] == self.Status.STABILIZING:
# we are already near target # we are already near target
@ -712,6 +551,7 @@ class Temp(PpmsMixin, Drivable):
class Field(PpmsMixin, Drivable): class Field(PpmsMixin, Drivable):
"""magnetic field""" """magnetic field"""
field = CmdHandler('field', 'FIELD?', '%g,%g,%d,%d')
Status = Enum(Drivable.Status, Status = Enum(Drivable.Status,
PREPARED = 150, PREPARED = 150,
PREPARING = 340, PREPARING = 340,
@ -724,20 +564,20 @@ class Field(PpmsMixin, Drivable):
parameters = { parameters = {
'value': 'value':
Override(datatype=FloatRange(unit='T'), poll=False, default=0), Override(datatype=FloatRange(unit='T'), poll=True),
'status': 'status':
Override(poll=False, datatype=StatusType(Status)), Override(datatype=StatusType(Status), poll=True),
'target': 'target':
Override(datatype=FloatRange(-15,15,unit='T'), poll=False), Override(datatype=FloatRange(-15,15,unit='T'), handler=field),
'ramp': 'ramp':
Parameter('ramping speed', readonly=False, poll=False, Parameter('ramping speed', readonly=False, handler=field,
datatype=FloatRange(0.064, 1.19, unit='T/min'), default=0.064), datatype=FloatRange(0.064, 1.19, unit='T/min')),
'approachmode': 'approachmode':
Parameter('how to approach target', readonly=False, poll=False, Parameter('how to approach target', readonly=False, handler=field,
datatype=EnumType(ApproachMode), default=0), datatype=EnumType(ApproachMode)),
'persistentmode': 'persistentmode':
Parameter('what to do after changing field', readonly=False, poll=False, Parameter('what to do after changing field', readonly=False, handler=field,
datatype=EnumType(PersistentMode), default=0), datatype=EnumType(PersistentMode)),
'pollinterval': 'pollinterval':
Override(visibility=3), Override(visibility=3),
} }
@ -756,7 +596,6 @@ class Field(PpmsMixin, Drivable):
} }
channel = 'field' channel = 'field'
_settingnames = ['target', 'ramp', 'approachmode', 'persistentmode']
_stopped = False _stopped = False
_last_target = 0 _last_target = 0
_last_change= 0 # means no target change is pending _last_change= 0 # means no target change is pending
@ -784,7 +623,7 @@ class Field(PpmsMixin, Drivable):
status = [self.Status.PREPARING, 'ramping leads'] status = [self.Status.PREPARING, 'ramping leads']
else: else:
status = [self.Status.WARN, 'timeout when ramping leads'] 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: if now > self._last_change + 5 or status != self._status_before_change:
self._last_change = 0 self._last_change = 0
self.log.debug('time needed to change to busy: %.3g', now - self._last_change) 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]] status = [self.Status.BUSY, 'changed target while %s' % status[1]]
else: else:
status = [self.Status.WARN, 'field status (%r) does not change to BUSY' % status] status = [self.Status.WARN, 'field status (%r) does not change to BUSY' % status]
self.status = status self.status = status
def _start(self): def analyze_field(self, target, ramp, approachmode, persistentmode):
"""common code for change target and change persistentmode""" if (target, ramp, approachmode, persistentmode) != self._last_settings:
self._last_change = time.time() # we update parameters only on change, as 'ramp' and 'approachmode' are
self._status_before_change = list(self.status) # 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): def change_field(self, new):
"""read settings if 'target' in new or 'persistentmode' in new:
# changed target or persistentmode
return the value for <pname> and update all other parameters if 'target' in new:
""" self._last_target = self.target # save for stop command
reply = self.get_reply('settings', 'FIELD?') self._stopped = False
if reply: self._last_change = time.time()
reply['target'] *= 1e-4 self._status_before_change = list(self.status)
reply['ramp'] *= 6e-3 else:
return self.apply_reply(reply, pname) # changed ramp or approachmode
if not self.isDriving():
def put_settings(self, value, pname): return None # nothing to be written, as this would trigger a ramp up of leads current
"""write settings, combining <pname>=<value> and current attributes return new.target * 1e+4, new.ramp / 6e-3, new.approachmode, new.persistentmode
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 do_stop(self): def do_stop(self):
if not isDriving(self.status): if not self.isDriving():
return return
self.status = [self.Status.IDLE, '_stopped'] self.status = [self.Status.IDLE, '_stopped']
self._stopped = True self._stopped = True
@ -884,20 +675,19 @@ class Field(PpmsMixin, Drivable):
class Position(PpmsMixin, Drivable): class Position(PpmsMixin, Drivable):
"""rotator position""" """rotator position"""
move = CmdHandler('move', 'MOVE?', '%g,%g,%g')
Status = Drivable.Status Status = Drivable.Status
parameters = { parameters = {
'value': 'value':
Override(datatype=FloatRange(unit='deg'), poll=False, default=0), Override(datatype=FloatRange(unit='deg'), poll=True),
'status':
Override(poll=False),
'target': 'target':
Override(datatype=FloatRange(-720., 720., unit='deg'), default=0., poll=False), Override(datatype=FloatRange(-720., 720., unit='deg'), handler=move),
'enabled': 'enabled':
Parameter('is this channel used?', readonly=False, poll=False, Parameter('is this channel used?', readonly=False, poll=False,
datatype=BoolType(), default=True), datatype=BoolType(), default=True),
'speed': 'speed':
Parameter('motor speed', readonly=False, poll=False, Parameter('motor speed', readonly=False, handler=move,
datatype=FloatRange(0.8, 12, unit='deg/sec'), default=12.0), datatype=FloatRange(0.8, 12, unit='deg/sec')),
'pollinterval': 'pollinterval':
Override(visibility=3), Override(visibility=3),
} }
@ -915,7 +705,6 @@ class Position(PpmsMixin, Drivable):
_stopped = False _stopped = False
_last_target = 0 _last_target = 0
_last_change = 0 # means no target change is pending _last_change = 0 # means no target change is pending
mode = 0 # always use normal mode
def update_value_status(self, value, packed_status): def update_value_status(self, value, packed_status):
"""update value and status""" """update value and status"""
@ -935,7 +724,7 @@ class Position(PpmsMixin, Drivable):
status = [self.Status.IDLE, 'stopped(%s)' % status[1]] status = [self.Status.IDLE, 'stopped(%s)' % status[1]]
if self._last_change: # there was a change, which is not yet confirmed by hw if self._last_change: # there was a change, which is not yet confirmed by hw
now = time.time() now = time.time()
if isDriving(status): if self.isDriving():
if now > self._last_change + 15 or status != self._status_before_change: if now > self._last_change + 15 or status != self._status_before_change:
self._last_change = 0 self._last_change = 0
self.log.debug('time needed to change to busy: %.3g', now - self._last_change) 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] status = [self.Status.WARN, 'temperature status (%r) does not change to BUSY' % status]
self.status = status self.status = status
def get_settings(self, pname): def analyze_move(self, target, mode, speed):
"""read settings 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 <pname> and update all other parameters def change_move(self, new):
""" speed = int(round(min(14, max(0, 15 - new.speed / 0.8)), 0))
reply = self.get_reply('settings', 'MOVE?') return new.target, 0, speed
if reply:
reply['speed'] = (15 - reply['speed']) * 0.8
reply.pop('mode', None)
return self.apply_reply(reply, pname)
def put_settings(self, value, pname): def write_target(self, target):
"""write settings, combining <pname>=<value> 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):
self._last_target = self.target # save for stop command self._last_target = self.target # save for stop command
self._stopped = False self._stopped = False
self._last_change = 0 self._last_change = 0
self._status_before_change = self.status self._status_before_change = self.status
return self.put_settings(value, 'target') return target
def write_speed(self, value): def write_speed(self, value):
if not isDriving(self.status): if self.isDriving():
return value 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): def do_stop(self):
if not isDriving(self.status): if not self.isDriving():
return return
self.status = [self.Status.BUSY, '_stopped'] self.status = [self.Status.BUSY, '_stopped']
self._stopped = True self._stopped = True

View File

@ -96,6 +96,12 @@ def test_CmdHandler():
CMDARGS = ['channel', 'loop'] CMDARGS = ['channel', 'loop']
CMDSEPARATOR ='|' 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') group1 = Hdl('group1', 'SIMPLE?', '%g')
group2 = Hdl('group2', 'CMD?%(channel)d', '%g,%s,%d') group2 = Hdl('group2', 'CMD?%(channel)d', '%g,%s,%d')