improved commandhandler
- analyze_<group> returns now a dict - change_<group> has no more values arguments. values may be read with change.readValues(), which leaves more freedom to the programmer, especially if the values do not need to be read before a change Change-Id: I67b70302eaf9bbdac107df61123062fa133d501d Reviewed-on: https://forge.frm2.tum.de/review/c/sine2020/secop/playground/+/22047 Tested-by: JenkinsCodeReview <bjoern_pedersen@frm2.tum.de> Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
This commit is contained in:
@ -45,7 +45,7 @@ class CmdHandler(secop.commandhandler.CmdHandler):
|
||||
|
||||
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')
|
||||
filterhdl = CmdHandler('filter', 'FILTER?%(channel)d', '%d,%d,%d')
|
||||
scan = CmdHandler('scan', 'SCAN?', '%d,%d')
|
||||
|
||||
|
||||
@ -74,10 +74,11 @@ class Main(HasIodev, Module):
|
||||
pollerClass = Poller
|
||||
|
||||
def analyze_scan(self, channel, autoscan):
|
||||
self.channel, self.autoscan = channel, autoscan
|
||||
return dict(channel=channel, autoscan=autoscan)
|
||||
|
||||
def change_scan(self, new, *args):
|
||||
return new.channel, new.autoscan
|
||||
def change_scan(self, change):
|
||||
change.readValues()
|
||||
return change.channel, change.autoscan
|
||||
|
||||
|
||||
class ResChannel(HasIodev, Readable):
|
||||
@ -121,7 +122,7 @@ class ResChannel(HasIodev, Readable):
|
||||
Parameter('current excitation', datatype=EnumType(off=0, **CUR_RANGE), readonly=False, handler=rdgrng),
|
||||
'vexc':
|
||||
Parameter('voltage excitation', datatype=EnumType(off=0, **VOLT_RANGE), readonly=False, handler=rdgrng),
|
||||
'enable':
|
||||
'enabled':
|
||||
Parameter('is this channel enabled?', datatype=BoolType(), readonly=False, handler=inset),
|
||||
'pause':
|
||||
Parameter('pause after channel change', datatype=IntRange(), readonly=False, handler=inset),
|
||||
@ -139,6 +140,9 @@ class ResChannel(HasIodev, Readable):
|
||||
def read_value(self):
|
||||
if self.channel != self._main.channel:
|
||||
return Done
|
||||
if not self.enabled:
|
||||
self.status = [self.Status.DISABLED, 'disabled']
|
||||
return Done
|
||||
result = self.sendRecv('RDGR?%d' % self.channel)
|
||||
result = float(result)
|
||||
if self.autorange == 'soft':
|
||||
@ -172,6 +176,8 @@ class ResChannel(HasIodev, Readable):
|
||||
def read_status(self):
|
||||
if self.channel != self._main.channel:
|
||||
return Done
|
||||
if not self.enabled:
|
||||
return [self.Status.DISABLED, 'disabled']
|
||||
result = int(self.sendRecv('RDGST?%d' % self.channel))
|
||||
result &= 0x37 # mask T_OVER and T_UNDER (change this when implementing temperatures instead of resistivities)
|
||||
statustext = STATUS_TEXT[result]
|
||||
@ -180,54 +186,56 @@ class ResChannel(HasIodev, Readable):
|
||||
return [self.Status.IDLE, '']
|
||||
|
||||
def analyze_rdgrng(self, iscur, exc, rng, autorange, excoff):
|
||||
if excoff:
|
||||
self.iexc, self.vexc = 0,0
|
||||
elif iscur:
|
||||
self.iexc, self.vexc = exc, 0
|
||||
else:
|
||||
self.iexc, self.vexc = 0, exc
|
||||
result = dict(range=rng)
|
||||
if autorange:
|
||||
self.autorange = 'hard'
|
||||
result['auotrange'] = 'hard'
|
||||
elif self.autorange == 'hard':
|
||||
result['autorange'] = 'soft'
|
||||
# else: do not change autorange
|
||||
if excoff:
|
||||
result.update(iexc=0, vexc=0)
|
||||
elif iscur:
|
||||
result.update(iexc=exc, vexc=0)
|
||||
else:
|
||||
if self.autorange == 'hard':
|
||||
self.autorange = 'soft'
|
||||
else:
|
||||
self.autorange = self.autorange
|
||||
self.range = rng
|
||||
result.update(iexc=0, vexc=exc)
|
||||
return result
|
||||
|
||||
def change_rdgrng(self, new, iscur, exc, rng, autorange, excoff):
|
||||
if new.vexc != self.vexc: # in case vext is changed, do not consider iexc
|
||||
new.iexc = 0
|
||||
if new.iexc != 0: # we need '!= 0' here, as bool(enum) is always True!
|
||||
def change_rdgrng(self, change):
|
||||
iscur, exc, rng, autorange, excoff = change.readValues()
|
||||
if change.doesInclude('vexc'): # in case vext is changed, do not consider iexc
|
||||
change.iexc = 0
|
||||
if change.iexc != 0: # we need '!= 0' here, as bool(enum) is always True!
|
||||
iscur = 1
|
||||
exc = new.iexc
|
||||
exc = change.iexc
|
||||
excoff = 0
|
||||
elif new.vexc != 0: # we need '!= 0' here, as bool(enum) is always True!
|
||||
elif change.vexc != 0: # we need '!= 0' here, as bool(enum) is always True!
|
||||
iscur = 0
|
||||
exc = new.vexc
|
||||
exc = change.vexc
|
||||
excoff = 0
|
||||
else:
|
||||
excoff = 1
|
||||
rng = new.range
|
||||
if new.autorange == 'hard':
|
||||
rng = change.range
|
||||
if change.autorange == 'hard':
|
||||
autorange = 1
|
||||
else:
|
||||
autorange = 0
|
||||
if new.autorange == 'soft':
|
||||
if rng < new.minrange:
|
||||
rng = new.minrange
|
||||
if change.autorange == 'soft':
|
||||
if rng < self.minrange:
|
||||
rng = self.minrange
|
||||
return iscur, exc, rng, autorange, excoff
|
||||
|
||||
def analyze_inset(self, on, dwell, pause, curve, tempco):
|
||||
self.enabled, self.dwell, self.pause = on, dwell, pause
|
||||
return dict(enabled=on, dwell=dwell, pause=pause)
|
||||
|
||||
def change_inset(self, new, on, dwell, pause, curve, tempco):
|
||||
return new.enable, new.dwell, new.pause, curve, tempco
|
||||
def change_inset(self, change):
|
||||
_, _, _, curve, tempco = change.readValues()
|
||||
return change.enabled, change.dwell, change.pause, curve, tempco
|
||||
|
||||
def analyze_filt(self, on, settle, window):
|
||||
self.filter = settle if on else 0
|
||||
def analyze_filter(self, on, settle, window):
|
||||
return dict(filter=settle if on else 0)
|
||||
|
||||
def change_filt(self, new, on, settle, window):
|
||||
if new.filter:
|
||||
return 1, new.filter, 80 # always use 80% filter
|
||||
def change_filter(self, change):
|
||||
_, settle, window = change.readValues()
|
||||
if change.filter:
|
||||
return 1, change.filter, 80 # always use 80% filter
|
||||
return 0, settle, window
|
||||
|
@ -29,12 +29,6 @@ The PPMS hardware has some special requirements:
|
||||
|
||||
Polling of value and status is done commonly for all modules. For each registered module
|
||||
<module>.update_value_status() is called in order to update their value and status.
|
||||
Polling of module settings is using the same poller (secop.Poller is checking iodev).
|
||||
Only the hidden (not exported) parameter 'settings' is polled, all the others are updated
|
||||
by read_settings. The modules parameters related to the settings are updated only on change.
|
||||
This allows for example for the field module to buffer ramp and approachmode until the
|
||||
next target or persistent_mode change happens, because sending the common command for
|
||||
settings and target would do a useless cycle of ramping up leads, heating switch etc.
|
||||
"""
|
||||
|
||||
import time
|
||||
@ -137,7 +131,6 @@ class PpmsMixin(HasIodev, Module):
|
||||
|
||||
pollerClass = Poller
|
||||
enabled = True # default, if no parameter enable is defined
|
||||
# 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
|
||||
@ -150,22 +143,19 @@ class PpmsMixin(HasIodev, Module):
|
||||
started_callback()
|
||||
|
||||
def read_value(self):
|
||||
"""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
|
||||
"""polling is done by the main module
|
||||
|
||||
and PPMS does not deliver really more fresh values when polled more often
|
||||
"""
|
||||
return Done
|
||||
|
||||
def read_status(self):
|
||||
"""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
|
||||
"""polling is done by the main module
|
||||
|
||||
and PPMS does not deliver really fresh status values anyway: the status is not
|
||||
changed immediately after a target change!
|
||||
"""
|
||||
return Done
|
||||
|
||||
def update_value_status(self, value, packed_status):
|
||||
"""update value and status
|
||||
@ -239,11 +229,11 @@ class DriverChannel(Channel):
|
||||
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
|
||||
return dict(current=current, powerlimit=powerlimit)
|
||||
|
||||
def change_drvout(self, new):
|
||||
return new.current, new.powerlimit
|
||||
def change_drvout(self, change):
|
||||
self.readValues()
|
||||
return change.current, change.powerlimit
|
||||
|
||||
|
||||
class BridgeChannel(Channel):
|
||||
@ -272,22 +262,23 @@ class BridgeChannel(Channel):
|
||||
Override(visibility=3),
|
||||
}
|
||||
|
||||
_settingnames = ['no', 'excitation', 'powerlimit', 'dcflag', 'readingmode', 'voltagelimit']
|
||||
|
||||
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 dict(
|
||||
enabled=excitation != 0 and powerlimit != 0 and voltagelimit != 0,
|
||||
excitation=excitation or self.excitation,
|
||||
powerlimit=powerlimit or self.powerlimit,
|
||||
dcflag=dcflag,
|
||||
readingmode=readingmode,
|
||||
voltagelimit=voltagelimit or self.voltagelimit,
|
||||
)
|
||||
|
||||
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
|
||||
def change_bridge(self, change):
|
||||
self.readValues()
|
||||
if change.enabled:
|
||||
return self.no, change.excitation, change.powerlimit, change.dcflag, change.readingmode, change.voltagelimit
|
||||
return self.no, 0, 0, change.dcflag, change.readingmode, 0
|
||||
|
||||
|
||||
class Level(PpmsMixin, Readable):
|
||||
@ -316,7 +307,7 @@ class Level(PpmsMixin, Readable):
|
||||
self.status = [self.Status.IDLE, '']
|
||||
else:
|
||||
self.status = [self.Status.ERROR, 'old reading']
|
||||
self.value = level
|
||||
return dict(value = level)
|
||||
|
||||
|
||||
class Chamber(PpmsMixin, Drivable):
|
||||
@ -377,7 +368,6 @@ class Chamber(PpmsMixin, Drivable):
|
||||
}
|
||||
|
||||
channel = 'chamber'
|
||||
_settingnames = ['target']
|
||||
|
||||
def update_value_status(self, value, packed_status):
|
||||
"""update value and status"""
|
||||
@ -385,16 +375,16 @@ class Chamber(PpmsMixin, Drivable):
|
||||
self.status = self.STATUS_MAP[self.value]
|
||||
|
||||
def analyze_chamber(self, target):
|
||||
self.target = target
|
||||
return dict(target=target)
|
||||
|
||||
def change_chamber(self, new):
|
||||
def change_chamber(self, change):
|
||||
"""write settings, combining <pname>=<value> and current attributes
|
||||
|
||||
and request updated settings
|
||||
"""
|
||||
if new.target == self.Operation.noop:
|
||||
if change.target == self.Operation.noop:
|
||||
return None
|
||||
return (new.target,)
|
||||
return (change.target,)
|
||||
|
||||
|
||||
class Temp(PpmsMixin, Drivable):
|
||||
@ -446,7 +436,7 @@ class Temp(PpmsMixin, Drivable):
|
||||
STATUS_MAP = {
|
||||
0: [Status.ERROR, 'unknown'],
|
||||
1: [Status.IDLE, 'stable at target'],
|
||||
2: [Status.RAMPING, 'changing'],
|
||||
2: [Status.RAMPING, 'ramping'],
|
||||
5: [Status.STABILIZING, 'within tolerance'],
|
||||
6: [Status.STABILIZING, 'outside tolerance'],
|
||||
10: [Status.WARN, 'standby'],
|
||||
@ -456,7 +446,6 @@ class Temp(PpmsMixin, Drivable):
|
||||
}
|
||||
|
||||
channel = 'temp'
|
||||
_settingnames = ['target', 'ramp', 'approachmode']
|
||||
_stopped = False
|
||||
_expected_target = 0
|
||||
_last_change = 0 # 0 means no target change is pending
|
||||
@ -481,15 +470,13 @@ 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 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)
|
||||
if now > self._last_change + 5:
|
||||
self._last_change = 0 # give up waiting for busy
|
||||
elif self.isDriving(status) and status != self._status_before_change:
|
||||
self.log.debug('time needed to change to busy: %.3g', now - self._last_change)
|
||||
self._last_change = 0
|
||||
else:
|
||||
if now < self._last_change + 15:
|
||||
status = [self.Status.BUSY, 'changed target while %s' % status[1]]
|
||||
else:
|
||||
status = [self.Status.WARN, 'temperature status (%r) does not change to BUSY' % status]
|
||||
status = [self.Status.BUSY, 'changed target']
|
||||
if self._expected_target:
|
||||
# handle timeout
|
||||
if self.isDriving(status):
|
||||
@ -500,24 +487,23 @@ class Temp(PpmsMixin, Drivable):
|
||||
self.status = status
|
||||
|
||||
def analyze_temp(self, target, ramp, approachmode):
|
||||
if (target, ramp, approachmode) != self._last_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 {}
|
||||
self._last_settings = target, ramp, approachmode
|
||||
return dict(target=target, ramp=ramp, approachmode=approachmode)
|
||||
|
||||
def change_temp(self, new):
|
||||
self.calc_expected(new.target, self.ramp)
|
||||
return new.target, new.ramp, new.approachmode
|
||||
def change_temp(self, change):
|
||||
self.calc_expected(change.target, self.ramp)
|
||||
return change.target, change.ramp, change.approachmode
|
||||
|
||||
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.status = [self.Status.BUSY, 'changed target']
|
||||
self._last_change = time.time()
|
||||
return target
|
||||
|
||||
@ -623,31 +609,27 @@ class Field(PpmsMixin, Drivable):
|
||||
status = [self.Status.PREPARING, 'ramping leads']
|
||||
else:
|
||||
status = [self.Status.WARN, 'timeout when ramping leads']
|
||||
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)
|
||||
elif now > self._last_change + 5:
|
||||
self._last_change = 0 # give up waiting for driving
|
||||
elif self.isDriving(status) and status != self._status_before_change:
|
||||
self._last_change = 0
|
||||
self.log.debug('time needed to change to busy: %.3g', now - self._last_change)
|
||||
else:
|
||||
if now < self._last_change + 5:
|
||||
status = [self.Status.BUSY, 'changed target while %s' % status[1]]
|
||||
else:
|
||||
status = [self.Status.WARN, 'field status (%r) does not change to BUSY' % status]
|
||||
status = [self.Status.BUSY, 'changed target']
|
||||
self.status = status
|
||||
|
||||
def analyze_field(self, target, ramp, approachmode, persistentmode):
|
||||
if (target, ramp, approachmode, persistentmode) != self._last_settings:
|
||||
if (target, ramp, approachmode, persistentmode) == self._last_settings:
|
||||
# we update parameters only on change, as 'ramp' and 'approachmode' are
|
||||
# not always sent to the hardware
|
||||
self._last_settings = target, ramp, approachmode, persistentmode
|
||||
self.target = target * 1e-4
|
||||
self.ramp = ramp * 6e-3
|
||||
self.approachmode = approachmode
|
||||
self.persistentmode = persistentmode
|
||||
return {}
|
||||
self._last_settings = target, ramp, approachmode, persistentmode
|
||||
return dict(target=target * 1e-4, ramp=ramp * 6e-3, approachmode=approachmode,
|
||||
persistentmode=persistentmode)
|
||||
|
||||
def change_field(self, new):
|
||||
if 'target' in new or 'persistentmode' in new:
|
||||
# changed target or persistentmode
|
||||
if 'target' in new:
|
||||
def change_field(self, change):
|
||||
if change.doesInclude('target', 'persistentmode'):
|
||||
if change.doesInclude('target'):
|
||||
self._last_target = self.target # save for stop command
|
||||
self._stopped = False
|
||||
self._last_change = time.time()
|
||||
@ -656,7 +638,7 @@ class Field(PpmsMixin, Drivable):
|
||||
# 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
|
||||
return change.target * 1e+4, change.ramp / 6e-3, change.approachmode, change.persistentmode
|
||||
|
||||
def do_stop(self):
|
||||
if not self.isDriving():
|
||||
@ -701,7 +683,6 @@ class Position(PpmsMixin, Drivable):
|
||||
}
|
||||
|
||||
channel = 'position'
|
||||
_settingnames = ['target', 'mode', 'speed']
|
||||
_stopped = False
|
||||
_last_target = 0
|
||||
_last_change = 0 # means no target change is pending
|
||||
@ -724,28 +705,26 @@ 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 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)
|
||||
if now > self._last_change + 5:
|
||||
self._last_change = 0 # give up waiting for busy
|
||||
elif self.isDriving() and status != self._status_before_change:
|
||||
self._last_change = 0
|
||||
self.log.debug('time needed to change to busy: %.3g', now - self._last_change)
|
||||
else:
|
||||
if now < self._last_change + 15:
|
||||
status = [self.Status.BUSY, 'changed target while %s' % status[1]]
|
||||
else:
|
||||
status = [self.Status.WARN, 'temperature status (%r) does not change to BUSY' % status]
|
||||
status = [self.Status.BUSY, 'changed target']
|
||||
self.status = status
|
||||
|
||||
def analyze_move(self, target, mode, speed):
|
||||
if (target, speed) != self._last_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 {}
|
||||
self._last_settings = target, speed
|
||||
return dict(target=target, speed=(15 - speed) * 0.8)
|
||||
|
||||
def change_move(self, new):
|
||||
speed = int(round(min(14, max(0, 15 - new.speed / 0.8)), 0))
|
||||
return new.target, 0, speed
|
||||
def change_move(self, change):
|
||||
speed = int(round(min(14, max(0, 15 - change.speed / 0.8)), 0))
|
||||
return change.target, 0, speed
|
||||
|
||||
def write_target(self, target):
|
||||
self._last_target = self.target # save for stop command
|
||||
|
Reference in New Issue
Block a user