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())]
class ChangeWrapper:
"""Wrapper around a module
class Change:
"""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.
In addition '<parameter>' indicates, whether <parameter> is to be changed.
setting new.<parameter> does not yet set the value on the module.
In addition '<parameter>' in new indicates, whether <parameter> is to be changed.
new.<parameter> 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_<param>, valuedict contains only one item:
the parameter to be changed.
When called from initialization, valuedict may have more items.
When called from write_<param>, 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_<group>
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_<group> 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_<group> and change_<group>)
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):

View File

@ -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_<param> 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:

View File

@ -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):

View File

@ -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')

View File

@ -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 <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')
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 <pname> 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 <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')
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 <pname> 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 <pname> 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 <pname>=<value> 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 <pname> 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 <pname>=<value> 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 <pname> 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 <pname>=<value> 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 <pname> 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 <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):
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

View File

@ -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')