unify name and module on Attached property

- setting the attribute using the name of an attached module
- getting the attribute results in the module object

+ change names iodev to io, iodevClass to ioClass,
  sendRecv to communicate, HasIodev to HasIO

Change-Id: I200b63a5a7dc1453bf6ac998782b065645201900
Reviewed-on: https://forge.frm2.tum.de/review/c/sine2020/secop/playground/+/27575
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
This commit is contained in:
2022-01-27 18:11:42 +01:00
parent b911bc1838
commit c1307cdd03
21 changed files with 301 additions and 209 deletions

View File

@ -93,6 +93,7 @@ def main(argv=None):
if args.relaxed:
generalConfig.defaults['lazy_number_validation'] = True
generalConfig.defaults['disable_value_range_check'] = True
generalConfig.defaults['legacy_hasiodev'] = True
generalConfig.init(args.gencfg)
logger.init(loglevel)

View File

@ -1,24 +1,23 @@
[node LscSIM.psi.ch]
[NODE]
id = LscSIM.psi.ch
description = Lsc Simulation at PSI
[interface tcp]
type = tcp
bindto = 0.0.0.0
bindport = 5000
[INTERFACE]
uri = tcp://5000
[module res]
[res]
class = secop_psi.ls370res.ResChannel
.channel = 3
.description = resistivity
.main = lsmain
.iodev = lscom
channel = 3
description = resistivity
main = lsmain
io = lscom
[module lsmain]
[lsmain]
class = secop_psi.ls370res.Main
.description = main control of Lsc controller
.iodev = lscom
description = main control of Lsc controller
io = lscom
[module lscom]
[lscom]
class = secop_psi.ls370sim.Ls370Sim
.description = simulated serial communicator to a LS 370
.visibility = 3
description = simulated serial communicator to a LS 370
visibility = 3

View File

@ -8,117 +8,117 @@ uri = tcp://5000
[tt]
class = secop_psi.ppms.Temp
description = main temperature
iodev = ppms
io = ppms
[mf]
class = secop_psi.ppms.Field
target.min = -9
target.max = 9
.description = magnetic field
.iodev = ppms
description = magnetic field
io = ppms
[pos]
class = secop_psi.ppms.Position
.description = sample rotator
.iodev = ppms
description = sample rotator
io = ppms
[lev]
class = secop_psi.ppms.Level
.description = helium level
.iodev = ppms
description = helium level
io = ppms
[chamber]
class = secop_psi.ppms.Chamber
.description = chamber state
.iodev = ppms
description = chamber state
io = ppms
[r1]
class = secop_psi.ppms.BridgeChannel
.description = resistivity channel 1
.no = 1
description = resistivity channel 1
no = 1
value.unit = Ohm
.iodev = ppms
io = ppms
[r2]
class = secop_psi.ppms.BridgeChannel
.description = resistivity channel 2
.no = 2
description = resistivity channel 2
no = 2
value.unit = Ohm
.iodev = ppms
io = ppms
[r3]
class = secop_psi.ppms.BridgeChannel
.description = resistivity channel 3
.no = 3
description = resistivity channel 3
no = 3
value.unit = Ohm
.iodev = ppms
io = ppms
[r4]
class = secop_psi.ppms.BridgeChannel
.description = resistivity channel 4
.no = 4
description = resistivity channel 4
no = 4
value.unit = Ohm
.iodev = ppms
io = ppms
[i1]
class = secop_psi.ppms.Channel
.description = current channel 1
.no = 1
description = current channel 1
no = 1
value.unit = uA
.iodev = ppms
io = ppms
[i2]
class = secop_psi.ppms.Channel
.description = current channel 2
.no = 2
description = current channel 2
no = 2
value.unit = uA
.iodev = ppms
io = ppms
[i3]
class = secop_psi.ppms.Channel
.description = current channel 3
.no = 3
description = current channel 3
no = 3
value.unit = uA
.iodev = ppms
io = ppms
[i4]
class = secop_psi.ppms.Channel
.description = current channel 4
.no = 4
description = current channel 4
no = 4
value.unit = uA
.iodev = ppms
io = ppms
[v1]
class = secop_psi.ppms.DriverChannel
.description = voltage channel 1
.no = 1
description = voltage channel 1
no = 1
value.unit = V
.iodev = ppms
io = ppms
[v2]
class = secop_psi.ppms.DriverChannel
.description = voltage channel 2
.no = 2
description = voltage channel 2
no = 2
value.unit = V
.iodev = ppms
io = ppms
[tv]
class = secop_psi.ppms.UserChannel
.description = VTI temperature
description = VTI temperature
enabled = 1
value.unit = K
.iodev = ppms
io = ppms
[ts]
class = secop_psi.ppms.UserChannel
.description = sample temperature
description = sample temperature
enabled = 1
value.unit = K
.iodev = ppms
io = ppms
[ppms]
class = secop_psi.ppms.Main
.description = the main and poller module
.class_id = QD.MULTIVU.PPMS.1
.visibility = 3
description = the main and poller module
class_id = QD.MULTIVU.PPMS.1
visibility = 3
pollinterval = 2

View File

@ -59,7 +59,7 @@ Communication
:show-inheritance:
:members: communicate, multicomm
.. autoclass:: secop.io.HasIodev
.. autoclass:: secop.io.HasIO
:show-inheritance:
.. autoclass:: secop.iohandler.IOHandlerBase

View File

@ -22,7 +22,7 @@ CCU4 luckily has a very simple and logical protocol:
.. code:: python
# the most common Frappy classes can be imported from secop.core
from secop.core import Readable, Parameter, FloatRange, BoolType, StringIO, HasIodev
from secop.core import Readable, Parameter, FloatRange, BoolType, StringIO, HasIO
class CCU4IO(StringIO):
@ -34,14 +34,13 @@ CCU4 luckily has a very simple and logical protocol:
identification = [('cid', r'CCU4.*')]
# inheriting the HasIodev mixin creates us a private attribute *_iodev*
# for talking with the hardware
# inheriting HasIO allows us to use the communicate method for talking with the hardware
# Readable as a base class defines the value and status parameters
class HeLevel(HasIodev, Readable):
class HeLevel(HasIO, Readable):
"""He Level channel of CCU4"""
# define the communication class to create the IO module
iodevClass = CCU4IO
ioClass = CCU4IO
# define or alter the parameters
# as Readable.value exists already, we give only the modified property 'unit'
@ -49,7 +48,7 @@ CCU4 luckily has a very simple and logical protocol:
def read_value(self):
# method for reading the main value
reply = self._iodev.communicate('h') # send 'h\n' and get the reply 'h=<value>\n'
reply = self.communicate('h') # send 'h\n' and get the reply 'h=<value>\n'
name, txtvalue = reply.split('=')
assert name == 'h' # check that we got a reply to our command
return txtvalue # the framework will automatically convert the string to a float
@ -115,17 +114,17 @@ the status codes from the hardware to the standard SECoP status codes.
}
def read_status(self):
name, txtvalue = self._iodev.communicate('hsf').split('=')
name, txtvalue = self.communicate('hsf').split('=')
assert name == 'hsf'
return self.STATUS_MAP(int(txtvalue))
def read_empty_length(self):
name, txtvalue = self._iodev.communicate('hem').split('=')
name, txtvalue = self.communicate('hem').split('=')
assert name == 'hem'
return txtvalue
def write_empty_length(self, value):
name, txtvalue = self._iodev.communicate('hem=%g' % value).split('=')
name, txtvalue = self.communicate('hem=%g' % value).split('=')
assert name == 'hem'
return txtvalue
@ -152,7 +151,7 @@ which means it might be worth to create a *query* method, and then the
for changing a parameter
:returns: the (new) value of the parameter
"""
name, txtvalue = self._iodev.communicate(cmd).split('=')
name, txtvalue = self.communicate(cmd).split('=')
assert name == cmd.split('=')[0] # check that we got a reply to our command
return txtvalue # Frappy will automatically convert the string to the needed data type

View File

@ -36,5 +36,5 @@ from secop.params import Command, Parameter
from secop.poller import AUTO, DYNAMIC, REGULAR, SLOW
from secop.properties import Property
from secop.proxy import Proxy, SecNode, proxy_class
from secop.io import HasIodev, StringIO, BytesIO
from secop.io import HasIO, StringIO, BytesIO, HasIodev # TODO: remove HasIodev (legacy stuff)
from secop.persistent import PersistentMixin, PersistentParam

View File

@ -29,56 +29,79 @@ import time
import threading
from secop.lib.asynconn import AsynConn, ConnectionClosed
from secop.datatypes import ArrayOf, BLOBType, BoolType, FloatRange, IntRange, StringType, TupleOf, ValueType
from secop.errors import CommunicationFailedError, CommunicationSilentError, ConfigError
from secop.datatypes import ArrayOf, BLOBType, BoolType, FloatRange, IntRange, \
StringType, TupleOf, ValueType
from secop.errors import CommunicationFailedError, CommunicationSilentError, \
ConfigError, ProgrammingError
from secop.modules import Attached, Command, \
Communicator, Done, Module, Parameter, Property
from secop.poller import REGULAR
from secop.lib import generalConfig
generalConfig.defaults['legacy_hasiodev'] = False
HEX_CODE = re.compile(r'[0-9a-fA-F][0-9a-fA-F]$')
class HasIodev(Module):
class HasIO(Module):
"""Mixin for modules using a communicator"""
iodev = Attached()
io = Attached()
uri = Property('uri for automatic creation of the attached communication module',
StringType(), default='')
iodevDict = {}
ioDict = {}
ioClass = None
def __init__(self, name, logger, opts, srv):
iodev = opts.get('iodev')
io = opts.get('io')
super().__init__(name, logger, opts, srv)
if self.uri:
opts = {'uri': self.uri, 'description': 'communication device for %s' % name,
'export': False}
ioname = self.iodevDict.get(self.uri)
ioname = self.ioDict.get(self.uri)
if not ioname:
ioname = iodev or name + '_iodev'
iodev = self.iodevClass(ioname, srv.log.getChild(ioname), opts, srv)
iodev.callingModule = []
srv.modules[ioname] = iodev
self.iodevDict[self.uri] = ioname
self.iodev = ioname
elif not self.iodev:
raise ConfigError("Module %s needs a value for either 'uri' or 'iodev'" % name)
ioname = io or name + '_io'
io = self.ioClass(ioname, srv.log.getChild(ioname), opts, srv) # pylint: disable=not-callable
io.callingModule = []
srv.modules[ioname] = io
self.ioDict[self.uri] = ioname
self.io = ioname
elif not io:
raise ConfigError("Module %s needs a value for either 'uri' or 'io'" % name)
def initModule(self):
try:
self._iodev.read_is_connected()
self.io.read_is_connected()
except (CommunicationFailedError, AttributeError):
# AttributeError: for missing _iodev?
# AttributeError: read_is_connected is not required for an io object
pass
super().initModule()
def communicate(self, *args):
return self._iodev.communicate(*args)
return self.io.communicate(*args)
def multicomm(self, *args):
return self._iodev.multicomm(*args)
return self.io.multicomm(*args)
sendRecv = communicate # TODO: remove legacy stuff
class HasIodev(HasIO):
# TODO: remove this legacy mixin
iodevClass = None
@property
def _iodev(self):
return self.io
def __init__(self, name, logger, opts, srv):
self.ioClass = self.iodevClass
super().__init__(name, logger, opts, srv)
if generalConfig.legacy_hasiodev:
self.log.warn('using the HasIodev mixin is deprecated - use HasIO instead')
else:
self.log.error('legacy HasIodev no longer supported')
self.log.error('you may suppress this error message by running the server with --relaxed')
raise ProgrammingError('legacy HasIodev no longer supported')
self.sendRecv = self.communicate
class IOBase(Communicator):

View File

@ -126,7 +126,7 @@ class CmdParser:
try:
argformat % ((0,) * len(casts)) # validate argformat
except ValueError as e:
raise ValueError("%s in %r" % (e, argformat))
raise ValueError("%s in %r" % (e, argformat)) from None
def format(self, *values):
return self.fmt % values
@ -242,7 +242,7 @@ class IOHandler(IOHandlerBase):
contain the command separator at the end.
"""
querycmd = self.make_query(module)
reply = module.sendRecv(changecmd + querycmd)
reply = module.communicate(changecmd + querycmd)
return self.parse_reply(reply)
def send_change(self, module, *values):
@ -253,7 +253,7 @@ class IOHandler(IOHandlerBase):
"""
changecmd = self.make_change(module, *values)
if self.CMDSEPARATOR is None:
module.sendRecv(changecmd) # ignore result
module.communicate(changecmd) # ignore result
return self.send_command(module)
return self.send_command(module, changecmd + self.CMDSEPARATOR)

View File

@ -771,19 +771,19 @@ class Communicator(HasComlog, Module):
class Attached(Property):
"""a special property, defining an attached modle
"""a special property, defining an attached module
assign a module name to this property in the cfg file,
and the server will create an attribute with this module
:param attrname: the name of the to be created attribute. if not given
the attribute name is the property name prepended by an underscore.
"""
# we can not put this to properties.py, as it needs datatypes
def __init__(self, attrname=None):
self.attrname = attrname
# we can not make it mandatory, as the check in Module.__init__ will be before auto-assign in HasIodev
super().__init__('attached module', StringType(), mandatory=False)
module = None
def __repr__(self):
return 'Attached(%s)' % (repr(self.attrname) if self.attrname else '')
def __init__(self, description='attached module'):
super().__init__(description, StringType(), mandatory=False)
def __get__(self, obj, owner):
if obj is None:
return self
if self.module is None:
self.module = obj.DISPATCHER.get_module(super().__get__(obj, owner))
return self.module

View File

@ -30,7 +30,7 @@ Usage examples:
pollerClass = poller.Poller
...
modules having a parameter 'iodev' with the same value will share the same poller
modules having a parameter 'io' with the same value will share the same poller
"""
import time
@ -58,11 +58,11 @@ class PollerBase:
table is a dict, with (<pollerClass>, <name>) as the key, and the
poller as value.
<name> is module.iodev or module.name, if iodev is not present
<name> is module.io.name or module.name, if io is not present
"""
# for modules with the same iodev, a common poller is used,
# modules without iodev all get their own poller
name = getattr(module, 'iodev', module.name)
# for modules with the same io, a common poller is used,
# modules without io all get their own poller
name = getattr(module, 'io', module).name
poller = table.get((cls, name), None)
if poller is None:
poller = cls(name)

View File

@ -29,17 +29,17 @@ from secop.lib import get_class
from secop.modules import Drivable, Module, Readable, Writable
from secop.params import Command, Parameter
from secop.properties import Property
from secop.io import HasIodev
from secop.io import HasIO
class ProxyModule(HasIodev, Module):
class ProxyModule(HasIO, Module):
module = Property('remote module name', datatype=StringType(), default='')
pollerClass = None
_consistency_check_done = False
_secnode = None
def iodevClass(self, name, logger, opts, srv):
def ioClass(self, name, logger, opts, srv):
opts['description'] = 'secnode %s on %s' % (opts.get('module', name), opts['uri'])
return SecNode(name, logger, opts, srv)
@ -54,7 +54,7 @@ class ProxyModule(HasIodev, Module):
def initModule(self):
if not self.module:
self.module = self.name
self._secnode = self._iodev.secnode
self._secnode = self.io.secnode
self._secnode.register_callback(self.module, self.updateEvent,
self.descriptiveDataChange, self.nodeStateChange)
super().initModule()
@ -227,5 +227,5 @@ def Proxy(name, logger, cfgdict, srv):
remote_class = cfgdict.pop('remote_class')
if 'description' not in cfgdict:
cfgdict['description'] = 'remote module %s on %s' % (
cfgdict.get('module', name), cfgdict.get('iodev', '?'))
cfgdict.get('module', name), cfgdict.get('io', '?'))
return proxy_class(remote_class)(name, logger, cfgdict, srv)

View File

@ -30,10 +30,9 @@ import sys
import traceback
from collections import OrderedDict
from secop.errors import ConfigError, SECoPError
from secop.errors import ConfigError
from secop.lib import formatException, get_class, generalConfig
from secop.lib.multievent import MultiEvent
from secop.modules import Attached
from secop.params import PREDEFINED_ACCESSIBLES
try:
@ -271,24 +270,17 @@ class Server:
for modname, modobj in self.modules.items():
self.log.info('registering module %r' % modname)
self.dispatcher.register_module(modobj, modname, modobj.export)
if modobj.pollerClass is not None:
# a module might be explicitly excluded from polling by setting pollerClass to None
modobj.pollerClass.add_to_table(poll_table, modobj)
# also call earlyInit on the modules
modobj.earlyInit()
if not modobj.earlyInitDone:
missing_super.add('%s was not called, probably missing super call'
% modobj.earlyInit.__qualname__)
# handle attached modules
# handle polling
for modname, modobj in self.modules.items():
for propname, propobj in modobj.propertyDict.items():
if isinstance(propobj, Attached):
try:
setattr(modobj, propobj.attrname or '_' + propname,
self.dispatcher.get_module(getattr(modobj, propname)))
except SECoPError as e:
errors.append('module %s, attached %s: %s' % (modname, propname, str(e)))
if modobj.pollerClass is not None:
# a module might be explicitly excluded from polling by setting pollerClass to None
modobj.pollerClass.add_to_table(poll_table, modobj)
# call init on each module after registering all
for modname, modobj in self.modules.items():

View File

@ -20,7 +20,7 @@
# *****************************************************************************
"""Andeen Hagerling capacitance bridge"""
from secop.core import Done, FloatRange, HasIodev, Parameter, Readable, StringIO
from secop.core import Done, FloatRange, HasIO, Parameter, Readable, StringIO
class Ah2700IO(StringIO):
@ -28,19 +28,19 @@ class Ah2700IO(StringIO):
timeout = 5
class Capacitance(HasIodev, Readable):
class Capacitance(HasIO, Readable):
value = Parameter('capacitance', FloatRange(unit='pF'), poll=True)
freq = Parameter('frequency', FloatRange(unit='Hz'), readonly=False, default=0)
voltage = Parameter('voltage', FloatRange(unit='V'), readonly=False, default=0)
loss = Parameter('loss', FloatRange(unit='deg'), default=0)
iodevClass = Ah2700IO
ioClass = Ah2700IO
def parse_reply(self, reply):
if reply.startswith('SI'): # this is an echo
self.sendRecv('SERIAL ECHO OFF')
reply = self.sendRecv('SI')
self.communicate('SERIAL ECHO OFF')
reply = self.communicate('SI')
if not reply.startswith('F='): # this is probably an error message like "LOSS TOO HIGH"
self.status = [self.Status.ERROR, reply]
return
@ -59,14 +59,14 @@ class Capacitance(HasIodev, Readable):
if lossunit == 'DS':
self.loss = loss
else: # the unit was wrong, we want DS = tan(delta), not NS = nanoSiemens
reply = self.sendRecv('UN DS').split() # UN DS returns a reply similar to SI
reply = self.communicate('UN DS').split() # UN DS returns a reply similar to SI
try:
self.loss = reply[7]
except IndexError:
pass # don't worry, loss will be updated next time
def read_value(self):
self.parse_reply(self.sendRecv('SI')) # SI = single trigger
self.parse_reply(self.communicate('SI')) # SI = single trigger
return Done
def read_freq(self):
@ -77,14 +77,14 @@ class Capacitance(HasIodev, Readable):
self.read_value()
return Done
def read_volt(self):
def read_voltage(self):
self.read_value()
return Done
def write_freq(self, value):
self.parse_reply(self.sendRecv('FR %g;SI' % value))
self.parse_reply(self.communicate('FR %g;SI' % value))
return Done
def write_volt(self, value):
self.parse_reply(self.sendRecv('V %g;SI' % value))
def write_voltage(self, value):
self.parse_reply(self.communicate('V %g;SI' % value))
return Done

View File

@ -23,7 +23,7 @@
"""drivers for CCU4, the cryostat control unit at SINQ"""
# the most common Frappy classes can be imported from secop.core
from secop.core import EnumType, FloatRange, \
HasIodev, Parameter, Readable, StringIO
HasIO, Parameter, Readable, StringIO
class CCU4IO(StringIO):
@ -34,14 +34,13 @@ class CCU4IO(StringIO):
identification = [('cid', r'CCU4.*')]
# inheriting the HasIodev mixin creates us a private attribute *_iodev*
# for talking with the hardware
# inheriting HasIO allows us to use the communicate method for talking with the hardware
# Readable as a base class defines the value and status parameters
class HeLevel(HasIodev, Readable):
class HeLevel(HasIO, Readable):
"""He Level channel of CCU4"""
# define the communication class to create the IO module
iodevClass = CCU4IO
ioClass = CCU4IO
# define or alter the parameters
# as Readable.value exists already, we give only the modified property 'unit'
@ -71,7 +70,7 @@ class HeLevel(HasIodev, Readable):
for changing a parameter
:returns: the (new) value of the parameter
"""
name, txtvalue = self._iodev.communicate(cmd).split('=')
name, txtvalue = self.communicate(cmd).split('=')
assert name == cmd.split('=')[0] # check that we got a reply to our command
return txtvalue # Frappy will automatically convert the string to the needed data type

View File

@ -23,7 +23,7 @@
not tested yet"""
from secop.core import Attached, BoolType, EnumType, FloatRange, \
HasIodev, Module, Parameter, StringIO, Writable
HasIO, Module, Parameter, StringIO, Writable
class K2601bIO(StringIO):
@ -41,7 +41,7 @@ SOURCECMDS = {
}
class SourceMeter(HasIodev, Module):
class SourceMeter(HasIO, Module):
resistivity = Parameter('readback resistivity', FloatRange(unit='Ohm'), poll=True)
power = Parameter('readback power', FloatRange(unit='W'), poll=True)
@ -49,19 +49,19 @@ class SourceMeter(HasIodev, Module):
readonly=False, default=0)
active = Parameter('output enable', BoolType(), readonly=False, poll=True)
iodevClass = K2601bIO
ioClass = K2601bIO
def read_resistivity(self):
return self.sendRecv('print(smua.measure.r())')
return self.communicate('print(smua.measure.r())')
def read_power(self):
return self.sendRecv('print(smua.measure.p())')
return self.communicate('print(smua.measure.p())')
def read_active(self):
return self.sendRecv('print(smua.source.output)')
return self.communicate('print(smua.source.output)')
def write_active(self, value):
return self.sendRecv('smua.source.output = %d print(smua.source.output)' % value)
return self.communicate('smua.source.output = %d print(smua.source.output)' % value)
# for now, mode will not be read from hardware
@ -69,11 +69,11 @@ class SourceMeter(HasIodev, Module):
if value == 0:
self.write_active(0)
else:
self.sendRecv(SOURCECMDS[value] + ' print(0)')
self.communicate(SOURCECMDS[value] + ' print(0)')
return value
class Current(HasIodev, Writable):
class Current(HasIO, Writable):
sourcemeter = Attached()
value = Parameter('measured current', FloatRange(unit='A'), poll=True)
@ -82,41 +82,41 @@ class Current(HasIodev, Writable):
limit = Parameter('current limit', FloatRange(0, 2.0, unit='A'), default=2, poll=True)
def read_value(self):
return self.sendRecv('print(smua.measure.i())')
return self.communicate('print(smua.measure.i())')
def read_target(self):
return self.sendRecv('print(smua.source.leveli)')
return self.communicate('print(smua.source.leveli)')
def write_target(self, value):
if not self.active:
raise ValueError('current source is disabled')
if value > self.limit:
raise ValueError('current exceeds limit')
return self.sendRecv('smua.source.leveli = %g print(smua.source.leveli)' % value)
return self.communicate('smua.source.leveli = %g print(smua.source.leveli)' % value)
def read_limit(self):
if self.active:
return self.limit
return self.sendRecv('print(smua.source.limiti)')
return self.communicate('print(smua.source.limiti)')
def write_limit(self, value):
if self.active:
return value
return self.sendRecv('smua.source.limiti = %g print(smua.source.limiti)' % value)
return self.communicate('smua.source.limiti = %g print(smua.source.limiti)' % value)
def read_active(self):
return self._sourcemeter.mode == 1 and self._sourcemeter.read_active()
return self.sourcemeter.mode == 1 and self.sourcemeter.read_active()
def write_active(self, value):
if self._sourcemeter.mode != 1:
if self.sourcemeter.mode != 1:
if value:
self._sourcemeter.write_mode(1) # switch to current
self.sourcemeter.write_mode(1) # switch to current
else:
return 0
return self._sourcemeter.write_active(value)
return self.sourcemeter.write_active(value)
class Voltage(HasIodev, Writable):
class Voltage(HasIO, Writable):
sourcemeter = Attached()
value = Parameter('measured voltage', FloatRange(unit='V'), poll=True)
@ -125,35 +125,35 @@ class Voltage(HasIodev, Writable):
limit = Parameter('current limit', FloatRange(0, 2.0, unit='V'), default=2, poll=True)
def read_value(self):
return self.sendRecv('print(smua.measure.v())')
return self.communicate('print(smua.measure.v())')
def read_target(self):
return self.sendRecv('print(smua.source.levelv)')
return self.communicate('print(smua.source.levelv)')
def write_target(self, value):
if not self.active:
raise ValueError('voltage source is disabled')
if value > self.limit:
raise ValueError('voltage exceeds limit')
return self.sendRecv('smua.source.levelv = %g print(smua.source.levelv)' % value)
return self.communicate('smua.source.levelv = %g print(smua.source.levelv)' % value)
def read_limit(self):
if self.active:
return self.limit
return self.sendRecv('print(smua.source.limitv)')
return self.communicate('print(smua.source.limitv)')
def write_limit(self, value):
if self.active:
return value
return self.sendRecv('smua.source.limitv = %g print(smua.source.limitv)' % value)
return self.communicate('smua.source.limitv = %g print(smua.source.limitv)' % value)
def read_active(self):
return self._sourcemeter.mode == 2 and self._sourcemeter.read_active()
return self.sourcemeter.mode == 2 and self.sourcemeter.read_active()
def write_active(self, value):
if self._sourcemeter.mode != 2:
if self.sourcemeter.mode != 2:
if value:
self._sourcemeter.write_mode(2) # switch to voltage
self.sourcemeter.write_mode(2) # switch to voltage
else:
return 0
return self._sourcemeter.write_active(value)
return self.sourcemeter.write_active(value)

View File

@ -28,7 +28,7 @@ from secop.lib import formatStatusBits
from secop.modules import Attached, Done, \
Drivable, Parameter, Property, Readable
from secop.poller import REGULAR, Poller
from secop.io import HasIodev
from secop.io import HasIO
Status = Drivable.Status
@ -58,7 +58,7 @@ class StringIO(secop.io.StringIO):
wait_before = 0.05
class Main(HasIodev, Drivable):
class Main(HasIO, Drivable):
value = Parameter('the current channel', poll=REGULAR, datatype=IntRange(0, 17))
target = Parameter('channel to select', datatype=IntRange(0, 17))
@ -66,7 +66,7 @@ class Main(HasIodev, Drivable):
pollinterval = Parameter(default=1, export=False)
pollerClass = Poller
iodevClass = StringIO
ioClass = StringIO
_channel_changed = 0 # time of last channel change
_channels = None # dict <channel no> of <module object>
@ -81,7 +81,7 @@ class Main(HasIodev, Drivable):
super().startModule(start_events)
for ch in range(1, 16):
if ch not in self._channels:
self.sendRecv('INSET %d,0,0,0,0,0;INSET?%d' % (ch, ch))
self.communicate('INSET %d,0,0,0,0,0;INSET?%d' % (ch, ch))
def read_value(self):
channel, auto = scan.send_command(self)
@ -114,7 +114,7 @@ class Main(HasIodev, Drivable):
def write_target(self, channel):
scan.send_change(self, channel, self.autoscan)
# self.sendRecv('SCAN %d,%d;SCAN?' % (channel, self.autoscan))
# self.communicate('SCAN %d,%d;SCAN?' % (channel, self.autoscan))
if channel != self.value:
self.value = 0
self._channel_changed = time.time()
@ -123,11 +123,11 @@ class Main(HasIodev, Drivable):
def write_autoscan(self, value):
scan.send_change(self, self.value, value)
# self.sendRecv('SCAN %d,%d;SCAN?' % (channel, self.autoscan))
# self.communicate('SCAN %d,%d;SCAN?' % (channel, self.autoscan))
return value
class ResChannel(HasIodev, Readable):
class ResChannel(HasIO, Readable):
"""temperature channel on Lakeshore 336"""
RES_RANGE = {key: i+1 for i, key in list(
@ -142,7 +142,7 @@ class ResChannel(HasIodev, Readable):
for val in [2, 6.32, 20, 63.2, 200, 632]))}
pollerClass = Poller
iodevClass = StringIO
ioClass = StringIO
_main = None # main module
_last_range_change = 0 # time of last range change
@ -183,7 +183,7 @@ class ResChannel(HasIodev, Readable):
return Done
# we got here, when we missed the idle state of self._main
self._trigger_read = False
result = self.sendRecv('RDGR?%d' % self.channel)
result = self.communicate('RDGR?%d' % self.channel)
result = float(result)
if self.autorange == 'soft':
now = time.time()
@ -216,9 +216,9 @@ class ResChannel(HasIodev, Readable):
def read_status(self):
if not self.enabled:
return [self.Status.DISABLED, 'disabled']
if self.channel != self._main.value:
if self.channel != self.main.value:
return Done
result = int(self.sendRecv('RDGST?%d' % self.channel))
result = int(self.communicate('RDGST?%d' % self.channel))
result &= 0x37 # mask T_OVER and T_UNDER (change this when implementing temperatures instead of resistivities)
statustext = ' '.join(formatStatusBits(result, STATUS_BIT_LABELS))
if statustext:
@ -283,5 +283,5 @@ class ResChannel(HasIodev, Readable):
def write_enabled(self, value):
inset.write(self, 'enabled', value)
if value:
self._main.write_target(self.channel)
self.main.write_target(self.channel)
return Done

View File

@ -40,10 +40,10 @@ from secop.datatypes import BoolType, EnumType, \
from secop.errors import HardwareError
from secop.lib import clamp
from secop.lib.enum import Enum
from secop.modules import Attached, Communicator, Done, \
from secop.modules import Communicator, Done, \
Drivable, Parameter, Property, Readable
from secop.poller import Poller
from secop.io import HasIodev
from secop.io import HasIO
try:
import secop_psi.ppmswindows as ppmshw
@ -130,10 +130,8 @@ class Main(Communicator):
return data # return data as string
class PpmsBase(HasIodev, Readable):
class PpmsBase(HasIO, Readable):
"""common base for all ppms modules"""
iodev = Attached()
# polling is done by the main module
# and PPMS does not deliver really more fresh values when polled more often
value = Parameter(poll=False, needscfg=False)
@ -150,7 +148,7 @@ class PpmsBase(HasIodev, Readable):
def initModule(self):
super().initModule()
self._iodev.register(self)
self.io.register(self)
def update_value_status(self, value, packed_status):
# update value and status
@ -197,7 +195,7 @@ class UserChannel(Channel):
datatype=StringType(), export=False, default='')
def write_enabled(self, enabled):
other = self._iodev.modules.get(self.linkenable, None)
other = self.io.modules.get(self.linkenable, None)
if other:
other.enabled = enabled
return enabled

View File

@ -26,7 +26,7 @@ import time
import struct
from secop.core import BoolType, Command, EnumType, FloatRange, IntRange, \
HasIodev, Parameter, Property, Drivable, PersistentMixin, PersistentParam, Done
HasIO, Parameter, Property, Drivable, PersistentMixin, PersistentParam, Done
from secop.io import BytesIO
from secop.errors import CommunicationFailedError, HardwareError, BadValueError, IsBusyError
from secop.rwhandler import ReadHandler, WriteHandler
@ -77,7 +77,7 @@ def writable(*args, **kwds):
return PersistentParam(*args, readonly=False, poll=True, initwrite=True, **kwds)
class Motor(PersistentMixin, HasIodev, Drivable):
class Motor(PersistentMixin, HasIO, Drivable):
address = Property('module address', IntRange(0, 255), default=1)
value = Parameter('motor position', FloatRange(unit='deg', fmtstr='%.3f'))
@ -116,7 +116,7 @@ class Motor(PersistentMixin, HasIodev, Drivable):
readonly=False, default=0, poll=True, visibility=3, group='more')
pollinterval = Parameter(group='more')
iodevClass = BytesIO
ioClass = BytesIO
fast_pollfactor = 0.001 # poll as fast as possible when busy
_started = 0
_calcTimeout = True
@ -131,20 +131,20 @@ class Motor(PersistentMixin, HasIodev, Drivable):
:param value: if given, the parameter is written, else it is returned
:return: the returned value
"""
if self._calcTimeout and self._iodev._conn:
if self._calcTimeout and self.io._conn:
self._calcTimeout = False
baudrate = getattr(self._iodev._conn.connection, 'baudrate', None)
baudrate = getattr(self.io._conn.connection, 'baudrate', None)
if baudrate:
if baudrate not in BAUDRATES:
raise CommunicationFailedError('unsupported baud rate: %d' % baudrate)
self._iodev.timeout = 0.03 + 200 / baudrate
self.io.timeout = 0.03 + 200 / baudrate
exc = None
byt = struct.pack('>BBBBi', self.address, cmd, adr, bank, round(value))
byt += bytes([sum(byt) & 0xff])
for itry in range(3,0,-1):
try:
reply = self._iodev.communicate(byt, 9)
reply = self.communicate(byt, 9)
if sum(reply[:-1]) & 0xff != reply[-1]:
raise CommunicationFailedError('checksum error')
# will try again

78
test/test_attach.py Normal file
View File

@ -0,0 +1,78 @@
# -*- coding: utf-8 -*-
# *****************************************************************************
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Module authors:
# Markus Zolliker <markus.zolliker@psi.ch>
#
# *****************************************************************************
from secop.modules import Module, Attached
from secop.protocol.dispatcher import Dispatcher
# class DispatcherStub:
# # omit_unchanged_within = 0
#
# # def __init__(self, updates):
# # self.updates = updates
# #
# # def announce_update(self, modulename, pname, pobj):
# # self.updates.setdefault(modulename, {})
# # if pobj.readerror:
# # self.updates[modulename]['error', pname] = str(pobj.readerror)
# # else:
# # self.updates[modulename][pname] = pobj.value
#
# def __init__(self):
# self.modules = {}
#
# def get_module(self, name):
# return self.modules[name]
#
# def register_module(self, name, module):
# self.modules[name] = module
class LoggerStub:
def debug(self, fmt, *args):
print(fmt % args)
info = warning = exception = debug
handlers = []
logger = LoggerStub()
class ServerStub:
restart = None
shutdown = None
def __init__(self):
self.dispatcher = Dispatcher('dispatcher', logger, {}, self)
def test_attach():
class Mod(Module):
att = Attached()
srv = ServerStub()
a = Module('a', logger, {'description': ''}, srv)
m = Mod('m', logger, {'description': '', 'att': 'a'}, srv)
assert m.propertyValues['att'] == 'a'
srv.dispatcher.register_module(a, 'a')
srv.dispatcher.register_module(m, 'm')
assert m.att == a

View File

@ -120,7 +120,7 @@ def test_IOHandler():
real = Parameter('a float value', FloatRange(), default=12.3, handler=group2, readonly=False)
text = Parameter('a string value', StringType(), default='x', handler=group2, readonly=False)
def sendRecv(self, command):
def communicate(self, command):
assert data.pop('command') == command
return data.pop('reply')
@ -146,7 +146,7 @@ def test_IOHandler():
print(updates)
updates.clear() # get rid of updates from initialisation
# for sendRecv
# for communicate
data.push('command', 'SIMPLE?')
data.push('reply', '4.51')
# for analyze_group1
@ -159,7 +159,7 @@ def test_IOHandler():
assert updates.pop('simple') == 45.1
assert not updates
# for sendRecv
# for communicate
data.push('command', 'CMD?3')
data.push('reply', '1.23,text,5')
# for analyze_group2
@ -172,7 +172,7 @@ def test_IOHandler():
assert data.empty()
assert not updates
# for sendRecv
# for communicate
data.push('command', 'CMD?3')
data.push('reply', '1.23,text,5')
# for analyze_group2
@ -183,7 +183,7 @@ def test_IOHandler():
data.push('self', 12.3, 'string')
data.push('new', 12.3, 'FOO')
data.push('changed', 1.23, 'foo', 9)
# for sendRecv
# for communicate
data.push('command', 'CMD 3,1.23,foo,9|CMD?3')
data.push('reply', '1.23,foo,9')
# for analyze_group2

View File

@ -117,7 +117,10 @@ class Parameter:
class Module:
properties = {}
pollerClass = Poller
iodev = 'common_iodev'
class io:
name = 'common_io'
def __init__(self, name, pollinterval=5, fastfactor=0.25, slowfactor=4, busy=False,
counts=(), auto=None):
'''create a dummy module
@ -203,7 +206,7 @@ def test_Poller(modules):
count[pobj.polltype] += 1
pobj.reset()
assert len(pollTable) == 1
poller = pollTable[(Poller, 'common_iodev')]
poller = pollTable[(Poller, 'common_io')]
artime.stop = poller.stop
poller._event = Event() # patch Event.wait