diff --git a/etc/ccr12.cfg b/etc/ccr12.cfg
new file mode 100644
index 0000000..6f35940
--- /dev/null
+++ b/etc/ccr12.cfg
@@ -0,0 +1,45 @@
+[equipment]
+id=ccr12
+
+[interface tcp]
+interface=tcp
+bindto=0.0.0.0
+bindport=10767
+# protocol to use for this interface
+framing=eol
+encoding=demo
+
+[device automatik]
+class=secop_mlz.entangle.NamedDigitalOutput
+tangodevice=tango://ccr12:10000/box/plc/_automatik
+mapping=dict(Off=0,p1=1,p2=2)
+
+[device compressor]
+class=secop_mlz.entangle.NamedDigitalOutput
+tangodevice=tango://ccr12:10000/box/plc/_cooler_onoff
+mapping=dict(Off=0,On=1)
+
+[device gas]
+class=secop_mlz.entangle.NamedDigitalOutput
+tangodevice=tango://ccr12:10000/box/plc/_gas_onoff
+mapping=dict(Off=0,On=1)
+
+[device vacuum]
+class=secop_mlz.entangle.NamedDigitalOutput
+tangodevice=tango://ccr12:10000/box/plc/_vacuum_onoff
+mapping=dict(Off=0,On=1)
+
+[device p1]
+class=secop_mlz.entangle.AnalogInput
+tangodevice=tango://ccr12:10000/box/plc/_p1
+
+[device p2]
+class=secop_mlz.entangle.AnalogInput
+tangodevice=tango://ccr12:10000/box/plc/_p2
+
+[device curve_p2]
+class=secop_mlz.entangle.NamedDigitalInput
+tangodevice=tango://ccr12:10000/box/plc/_curve
+value.default='undefined'
+mapping=dict(curve1=1,curve2=2,curve3=3)
+
diff --git a/etc/cryo.cfg b/etc/cryo.cfg
index ab4467c..474e8b2 100644
--- a/etc/cryo.cfg
+++ b/etc/cryo.cfg
@@ -5,6 +5,7 @@ description = short description
This is a very long description providing all the glory details in all the glory details about the stuff we are describing
+
[interface tcp]
interface=tcp
bindto=0.0.0.0
@@ -18,7 +19,7 @@ encoding=demo
# some (non-defaut) module properties
.group=very important/stuff
# class of module:
-class=devices.cryo.Cryostat
+class=secop_demo.cryo.Cryostat
# some parameters
jitter=0.1
diff --git a/etc/demo.cfg b/etc/demo.cfg
index 0f4f894..59d8f80 100644
--- a/etc/demo.cfg
+++ b/etc/demo.cfg
@@ -10,34 +10,34 @@ framing=eol
encoding=demo
[device heatswitch]
-class=devices.demo.Switch
+class=secop_demo.demo.Switch
switch_on_time=5
switch_off_time=10
[device mf]
-class=devices.demo.MagneticField
+class=secop_demo.demo.MagneticField
heatswitch = heatswitch
[device ts]
-class=devices.demo.SampleTemp
+class=secop_demo.demo.SampleTemp
sensor = 'Q1329V7R3'
ramp = 4
target = 10
default = 10
[device tc1]
-class=devices.demo.CoilTemp
+class=secop_demo.demo.CoilTemp
sensor="X34598T7"
[device tc2]
-class=devices.demo.CoilTemp
+class=secop_demo.demo.CoilTemp
sensor="X39284Q8'
[device label]
-class=devices.demo.Label
+class=secop_demo.demo.Label
system=Cryomagnet MX15
subdev_mf=mf
subdev_ts=ts
#[device vt]
-#class=devices.demo.ValidatorTest
+#class=secop_demo.demo.ValidatorTest
diff --git a/etc/epics.cfg b/etc/epics.cfg
index 69a7dab..159b3f0 100644
--- a/etc/epics.cfg
+++ b/etc/epics.cfg
@@ -17,23 +17,23 @@ framing=eol
encoding=demo
[device tc1]
-class=devices.demo.CoilTemp
+class=secop_demo.demo.CoilTemp
sensor="X34598T7"
[device tc2]
-class=devices.demo.CoilTemp
+class=secop_demo.demo.CoilTemp
sensor="X39284Q8'
[device sensor1]
-class=devices.epics.EpicsReadable
+class=secop_ess.epics.EpicsReadable
epics_version="v4"
.group="Lakeshore336"
value_pv="DEV:KRDG1"
[device loop1]
-class=devices.epics.EpicsTempCtrl
+class=secop_ess.epics.EpicsTempCtrl
epics_version="v4"
.group="Lakeshore336"
@@ -43,14 +43,14 @@ heaterrange_pv="DEV:RANGE_S1"
[device sensor2]
-class=devices.epics.EpicsReadable
+class=secop_ess.epics.EpicsReadable
epics_version="v4"
.group="Lakeshore336"
value_pv="DEV:KRDG2"
[device loop2]
-class=devices.epics.EpicsTempCtrl
+class=secop_ess.epics.EpicsTempCtrl
epics_version="v4"
.group="Lakeshore336"
diff --git a/etc/test.cfg b/etc/test.cfg
index 16ece1b..a9603cc 100644
--- a/etc/test.cfg
+++ b/etc/test.cfg
@@ -11,21 +11,21 @@ encoding=demo
[device LN2]
-class=devices.test.LN2
+class=secop_demo.test.LN2
[device heater]
-class=devices.test.Heater
+class=secop_demo.test.Heater
maxheaterpower=10
[device T1]
-class=devices.test.Temp
+class=secop_demo.test.Temp
sensor="X34598T7"
[device T2]
-class=devices.demo.CoilTemp
+class=secop_demo.demo.CoilTemp
sensor="X34598T8"
[device T3]
-class=devices.demo.CoilTemp
+class=secop_demo.demo.CoilTemp
sensor="X34598T9"
diff --git a/secop/client/baseclient.py b/secop/client/baseclient.py
index 080a63a..91a3b95 100644
--- a/secop/client/baseclient.py
+++ b/secop/client/baseclient.py
@@ -156,6 +156,7 @@ class Client(object):
self.log = mlzlog.log.getChild('client', True)
else:
class logStub(object):
+
def info(self, *args):
pass
debug = info
@@ -343,14 +344,17 @@ class Client(object):
def _issueDescribe(self):
_, self.equipment_id, describing_data = self._communicate('describe')
try:
- describing_data = self._decode_substruct(['modules'], describing_data)
+ describing_data = self._decode_substruct(
+ ['modules'], describing_data)
for modname, module in describing_data['modules'].items():
- describing_data['modules'][modname] = self._decode_substruct(['parameters', 'commands'], module)
+ describing_data['modules'][modname] = self._decode_substruct(
+ ['parameters', 'commands'], module)
self.describing_data = describing_data
for module, moduleData in self.describing_data['modules'].items():
- for parameter, parameterData in moduleData['parameters'].items():
+ for parameter, parameterData in moduleData[
+ 'parameters'].items():
datatype = get_datatype(parameterData['datatype'])
self.describing_data['modules'][module]['parameters'] \
[parameter]['datatype'] = datatype
diff --git a/secop/datatypes.py b/secop/datatypes.py
index e7d1a4d..46726be 100644
--- a/secop/datatypes.py
+++ b/secop/datatypes.py
@@ -22,12 +22,21 @@
"""Define validated data types."""
-
from .errors import ProgrammingError
from collections import OrderedDict
+# Only export these classes for 'from secop.datatypes import *'
+__all__ = [
+ "DataType",
+ "FloatRange", "IntRange",
+ "BoolType", "EnumType",
+ "BLOBType", "StringType",
+ "TupleOf", "ArrayOf", "StructOf",
+]
# base class for all DataTypes
+
+
class DataType(object):
as_json = ['undefined']
@@ -39,6 +48,11 @@ class DataType(object):
"""returns a python object fit for external serialisation or logging"""
raise NotImplemented
+ def from_string(self, text):
+ """interprets a given string and returns a validated (internal) value"""
+ # to evaluate values from configfiles, etc...
+ raise NotImplemented
+
# goodie: if called, validate
def __call__(self, value):
return self.validate(value)
@@ -52,7 +66,7 @@ class FloatRange(DataType):
self.max = None if max is None else float(max)
# note: as we may compare to Inf all comparisons would be false
if (self.min or float('-inf')) <= (self.max or float('+inf')):
- if min == None and max == None:
+ if min is None and max is None:
self.as_json = ['double']
else:
self.as_json = ['double', min, max]
@@ -65,9 +79,11 @@ class FloatRange(DataType):
except:
raise ValueError('Can not validate %r to float' % value)
if self.min is not None and value < self.min:
- raise ValueError('%r should not be less then %s' % (value, self.min))
+ raise ValueError('%r should not be less then %s' %
+ (value, self.min))
if self.max is not None and value > self.max:
- raise ValueError('%r should not be greater than %s' % (value, self.max))
+ raise ValueError('%r should not be greater than %s' %
+ (value, self.max))
if None in (self.min, self.max):
return value
if self.min <= value <= self.max:
@@ -76,9 +92,10 @@ class FloatRange(DataType):
(value, self.min, self.max))
def __repr__(self):
- if self.max != None:
- return "FloatRange(%r, %r)" % (float('-inf') if self.min is None else self.min, self.max)
- if self.min != None:
+ if self.max is not None:
+ return "FloatRange(%r, %r)" % (
+ float('-inf') if self.min is None else self.min, self.max)
+ if self.min is not None:
return "FloatRange(%r)" % self.min
return "FloatRange()"
@@ -86,15 +103,20 @@ class FloatRange(DataType):
"""returns a python object fit for serialisation"""
return float(value)
+ def from_string(self, text):
+ value = float(text)
+ return self.validate(value)
+
class IntRange(DataType):
"""Restricted int type"""
+
def __init__(self, min=None, max=None):
self.min = int(min) if min is not None else min
self.max = int(max) if max is not None else max
if self.min is not None and self.max is not None and self.min > self.max:
raise ValueError('Max must be larger then min!')
- if self.min == None and self.max == None:
+ if self.min is None and self.max is None:
self.as_json = ['int']
else:
self.as_json = ['int', self.min, self.max]
@@ -104,10 +126,10 @@ class IntRange(DataType):
value = int(value)
if self.min is not None and value < self.min:
raise ValueError('%r should be an int between %d and %d' %
- (value, self.min, self.max or 0))
+ (value, self.min, self.max or 0))
if self.max is not None and value > self.max:
raise ValueError('%r should be an int between %d and %d' %
- (value, self.min or 0, self.max))
+ (value, self.min or 0, self.max))
return value
except:
raise ValueError('Can not validate %r to int' % value)
@@ -123,15 +145,20 @@ class IntRange(DataType):
"""returns a python object fit for serialisation"""
return int(value)
+ def from_string(self, text):
+ value = int(text)
+ return self.validate(value)
+
class EnumType(DataType):
as_json = ['enum']
+
def __init__(self, *args, **kwds):
# enum keys are ints! check
self.entries = {}
num = 0
for arg in args:
- if type(arg) != str:
+ if not isinstance(arg, str):
print arg, type(arg)
raise ValueError('EnumType entries MUST be strings!')
self.entries[num] = arg
@@ -139,19 +166,24 @@ class EnumType(DataType):
for k, v in kwds.items():
v = int(v)
if v in self.entries:
- raise ValueError('keyword argument %r=%d is already assigned %r', k, v, self.entries[v])
+ raise ValueError(
+ 'keyword argument %r=%d is already assigned %r',
+ k,
+ v,
+ self.entries[v])
self.entries[v] = k
if len(self.entries) == 0:
raise ValueError('Empty enums ae not allowed!')
self.reversed = {}
- for k,v in self.entries.items():
+ for k, v in self.entries.items():
if v in self.reversed:
raise ValueError('Mapping for %r=%r is not Unique!', v, k)
self.reversed[v] = k
self.as_json = ['enum', self.reversed.copy()]
def __repr__(self):
- return "EnumType(%s)" % ', '.join(['%s=%d' % (v,k) for k,v in self.entries.items()])
+ return "EnumType(%s)" % ', '.join(
+ ['%s=%d' % (v, k) for k, v in self.entries.items()])
def export(self, value):
"""returns a python object fit for serialisation"""
@@ -159,7 +191,8 @@ class EnumType(DataType):
return self.reversed[value]
if int(value) in self.entries:
return int(value)
- raise ValueError('%r is not one of %s', str(value), ', '.join(self.reversed.keys()))
+ raise ValueError('%r is not one of %s', str(
+ value), ', '.join(self.reversed.keys()))
def validate(self, value):
"""return the validated (internal) value or raise"""
@@ -167,10 +200,16 @@ class EnumType(DataType):
return value
if int(value) in self.entries:
return self.entries[int(value)]
- raise ValueError('%r is not one of %s', str(value), ', '.join(map(str,self.entries.keys())))
+ raise ValueError('%r is not one of %s', str(value),
+ ', '.join(map(str, self.entries.keys())))
+
+ def from_string(self, text):
+ value = text
+ return self.validate(value)
class BLOBType(DataType):
+
def __init__(self, minsize=0, maxsize=None):
self.minsize = minsize
self.maxsize = maxsize
@@ -194,19 +233,26 @@ class BLOBType(DataType):
raise ValueError('%r has the wrong type!', value)
size = len(value)
if size < self.minsize:
- raise ValueError('%r must be at least %d bytes long!', value, self.minsize)
+ raise ValueError(
+ '%r must be at least %d bytes long!', value, self.minsize)
if self.maxsize is not None:
if size > self.maxsize:
- raise ValueError('%r must be at most %d bytes long!', value, self.maxsize)
+ raise ValueError(
+ '%r must be at most %d bytes long!', value, self.maxsize)
return value
def export(self, value):
"""returns a python object fit for serialisation"""
return b'%s' % value
+ def from_string(self, text):
+ value = text
+ return self.validate(value)
+
class StringType(DataType):
as_json = ['string']
+
def __init__(self, minsize=0, maxsize=None):
self.minsize = minsize
self.maxsize = maxsize
@@ -219,7 +265,8 @@ class StringType(DataType):
def __repr__(self):
if self.maxsize:
- return 'StringType(%s, %s)' % (str(self.minsize), str(self.maxsize))
+ return 'StringType(%s, %s)' % (
+ str(self.minsize), str(self.maxsize))
if self.minsize:
return 'StringType(%d)' % str(self.minsize)
return 'StringType()'
@@ -230,22 +277,31 @@ class StringType(DataType):
raise ValueError('%r has the wrong type!', value)
size = len(value)
if size < self.minsize:
- raise ValueError('%r must be at least %d bytes long!', value, self.minsize)
+ raise ValueError(
+ '%r must be at least %d bytes long!', value, self.minsize)
if self.maxsize is not None:
if size > self.maxsize:
- raise ValueError('%r must be at most %d bytes long!', value, self.maxsize)
+ raise ValueError(
+ '%r must be at most %d bytes long!', value, self.maxsize)
if '\0' in value:
- raise ValueError('Strings are not allowed to embed a \\0! Use a Blob instead!')
+ raise ValueError(
+ 'Strings are not allowed to embed a \\0! Use a Blob instead!')
return value
def export(self, value):
"""returns a python object fit for serialisation"""
return '%s' % value
+ def from_string(self, text):
+ value = text
+ return self.validate(value)
# Bool is a special enum
+
+
class BoolType(DataType):
as_json = ['bool']
+
def __repr__(self):
return 'BoolType()'
@@ -261,23 +317,31 @@ class BoolType(DataType):
"""returns a python object fit for serialisation"""
return True if self.validate(value) else False
+ def from_string(self, text):
+ value = text
+ return self.validate(value)
#
# nested types
#
+
+
class ArrayOf(DataType):
+
def __init__(self, subtype, minsize_or_size=None, maxsize=None):
if maxsize is None:
maxsize = minsize_or_size
self.minsize = minsize_or_size
self.maxsize = maxsize
if self.minsize is not None and self.maxsize is not None and \
- self.minsize > self.maxsize:
- raise ValueError('minsize must be less than or equal to maxsize!')
+ self.minsize > self.maxsize:
+ raise ValueError('minsize must be less than or equal to maxsize!')
if not isinstance(subtype, DataType):
- raise ValueError('ArrayOf only works with DataType objs as first argument!')
+ raise ValueError(
+ 'ArrayOf only works with DataType objs as first argument!')
self.subtype = subtype
- self.as_json = ['array', self.subtype.as_json, self.minsize, self.maxsize]
+ self.as_json = ['array', self.subtype.as_json,
+ self.minsize, self.maxsize]
if self.minsize is not None and self.minsize < 0:
raise ValueError('Minimum size must be >= 0!')
if self.maxsize is not None and self.maxsize < 1:
@@ -286,32 +350,43 @@ class ArrayOf(DataType):
raise ValueError('Maximum size must be >= Minimum size')
def __repr__(self):
- return 'ArrayOf(%s, %s, %s)' % (repr(self.subtype), self.minsize, self.maxsize)
+ return 'ArrayOf(%s, %s, %s)' % (
+ repr(self.subtype), self.minsize, self.maxsize)
def validate(self, value):
"""validate a external representation to an internal one"""
if isinstance(value, (tuple, list)):
# check number of elements
if self.minsize is not None and len(value) < self.minsize:
- raise ValueError('Array too small, needs at least %d elements!', self.minsize)
+ raise ValueError(
+ 'Array too small, needs at least %d elements!',
+ self.minsize)
if self.maxsize is not None and len(value) > self.maxsize:
- raise ValueError('Array too big, holds at most %d elements!', self.minsize)
+ raise ValueError(
+ 'Array too big, holds at most %d elements!', self.minsize)
# apply subtype valiation to all elements and return as list
return [self.subtype.validate(elem) for elem in value]
- raise ValueError('Can not convert %s to ArrayOf DataType!', repr(value))
+ raise ValueError(
+ 'Can not convert %s to ArrayOf DataType!', repr(value))
def export(self, value):
"""returns a python object fit for serialisation"""
return [self.subtype.export(elem) for elem in value]
+ def from_string(self, text):
+ value = eval(text) # XXX: !!!
+ return self.validate(value)
+
class TupleOf(DataType):
+
def __init__(self, *subtypes):
if not subtypes:
raise ValueError('Empty tuples are not allowed!')
for subtype in subtypes:
if not isinstance(subtype, DataType):
- raise ValueError('TupleOf only works with DataType objs as arguments!')
+ raise ValueError(
+ 'TupleOf only works with DataType objs as arguments!')
self.subtypes = subtypes
self.as_json = ['tuple', [subtype.as_json for subtype in subtypes]]
@@ -323,65 +398,84 @@ class TupleOf(DataType):
# keep the ordering!
try:
if len(value) != len(self.subtypes):
- raise ValueError('Illegal number of Arguments! Need %d arguments.', len(self.subtypes))
+ raise ValueError(
+ 'Illegal number of Arguments! Need %d arguments.', len(
+ self.subtypes))
# validate elements and return as list
- return [sub.validate(elem) for sub,elem in zip(self.subtypes, value)]
+ return [sub.validate(elem)
+ for sub, elem in zip(self.subtypes, value)]
except Exception as exc:
raise ValueError('Can not validate:', str(exc))
def export(self, value):
"""returns a python object fit for serialisation"""
- return [sub.export(elem) for sub,elem in zip(self.subtypes, value)]
+ return [sub.export(elem) for sub, elem in zip(self.subtypes, value)]
+
+ def from_string(self, text):
+ value = eval(text) # XXX: !!!
+ return self.validate(tuple(value))
class StructOf(DataType):
+
def __init__(self, **named_subtypes):
if not named_subtypes:
raise ValueError('Empty structs are not allowed!')
for name, subtype in named_subtypes.items():
if not isinstance(subtype, DataType):
- raise ProgrammingError('StructOf only works with named DataType objs as keyworded arguments!')
+ raise ProgrammingError(
+ 'StructOf only works with named DataType objs as keyworded arguments!')
if not isinstance(name, (str, unicode)):
- raise ProgrammingError('StructOf only works with named DataType objs as keyworded arguments!')
+ raise ProgrammingError(
+ 'StructOf only works with named DataType objs as keyworded arguments!')
self.named_subtypes = named_subtypes
- self.as_json = ['struct', dict((n,s.as_json) for n,s in named_subtypes.items())]
+ self.as_json = ['struct', dict((n, s.as_json)
+ for n, s in named_subtypes.items())]
def __repr__(self):
- return 'StructOf(%s)' % ', '.join(['%s=%s'%(n,repr(st)) for n,st in self.named_subtypes.iteritems()])
+ return 'StructOf(%s)' % ', '.join(
+ ['%s=%s' % (n, repr(st)) for n, st in self.named_subtypes.iteritems()])
def validate(self, value):
"""return the validated value or raise"""
try:
if len(value.keys()) != len(self.named_subtypes.keys()):
- raise ValueError('Illegal number of Arguments! Need %d arguments.', len(self.namd_subtypes.keys()))
+ raise ValueError(
+ 'Illegal number of Arguments! Need %d arguments.', len(
+ self.namd_subtypes.keys()))
# validate elements and return as dict
return dict((str(k), self.named_subtypes[k].validate(v))
- for k,v in value.items())
+ for k, v in value.items())
except Exception as exc:
- raise ValueError('Can not validate %s: %s', repr(value),str(exc))
+ raise ValueError('Can not validate %s: %s', repr(value), str(exc))
def export(self, value):
"""returns a python object fit for serialisation"""
if len(value.keys()) != len(self.named_subtypes.keys()):
- raise ValueError('Illegal number of Arguments! Need %d arguments.', len(self.namd_subtypes.keys()))
- return dict((str(k),self.named_subtypes[k].export(v))
- for k,v in value.items())
-
-
+ raise ValueError(
+ 'Illegal number of Arguments! Need %d arguments.', len(
+ self.namd_subtypes.keys()))
+ return dict((str(k), self.named_subtypes[k].export(v))
+ for k, v in value.items())
+ def from_string(self, text):
+ value = eval(text) # XXX: !!!
+ return self.validate(dict(value))
# XXX: derive from above classes automagically!
DATATYPES = dict(
- bool = lambda : BoolType(),
- int = lambda _min=None, _max=None: IntRange(_min, _max),
- double = lambda _min=None, _max=None: FloatRange(_min, _max),
- blob = lambda _min=None, _max=None: BLOBType(_min, _max),
- string = lambda _min=None, _max=None: StringType(_min, _max),
- array = lambda subtype, _min=None, _max=None: ArrayOf(get_datatype(subtype), _min, _max),
- tuple = lambda subtypes: TupleOf(*map(get_datatype,subtypes)),
- enum = lambda kwds: EnumType(**kwds),
- struct = lambda named_subtypes: StructOf(**dict((n,get_datatype(t)) for n,t in named_subtypes.items())),
+ bool=lambda: BoolType(),
+ int=lambda _min=None, _max=None: IntRange(_min, _max),
+ double=lambda _min=None, _max=None: FloatRange(_min, _max),
+ blob=lambda _min=None, _max=None: BLOBType(_min, _max),
+ string=lambda _min=None, _max=None: StringType(_min, _max),
+ array=lambda subtype, _min=None, _max=None: ArrayOf(
+ get_datatype(subtype), _min, _max),
+ tuple=lambda subtypes: TupleOf(*map(get_datatype, subtypes)),
+ enum=lambda kwds: EnumType(**kwds),
+ struct=lambda named_subtypes: StructOf(
+ **dict((n, get_datatype(t)) for n, t in named_subtypes.items())),
)
@@ -392,12 +486,14 @@ def export_datatype(datatype):
return datatype.as_json
# important for getting the right datatype from formerly jsonified descr.
+
+
def get_datatype(json):
if json is None:
return json
if not isinstance(json, list):
raise ValueError('Argument must be a properly formatted list!')
- if len(json)<1:
+ if len(json) < 1:
raise ValueError('can not validate %r', json)
base = json[0]
if base in DATATYPES:
diff --git a/secop/errors.py b/secop/errors.py
index 8600fd9..2791090 100644
--- a/secop/errors.py
+++ b/secop/errors.py
@@ -33,3 +33,11 @@ class ConfigError(SECoPServerError):
class ProgrammingError(SECoPServerError):
pass
+
+
+class CommunicationError(SECoPServerError):
+ pass
+
+
+class HardwareError(SECoPServerError):
+ pass
diff --git a/secop/gui/mainwindow.py b/secop/gui/mainwindow.py
index 5d3ae95..84d33a7 100644
--- a/secop/gui/mainwindow.py
+++ b/secop/gui/mainwindow.py
@@ -71,7 +71,6 @@ class MainWindow(QMainWindow):
loadUi(self, 'mainwindow.ui')
self.toolBar.hide()
- self.lineEdit.hide()
self.splitter.setStretchFactor(0, 1)
self.splitter.setStretchFactor(1, 70)
@@ -108,6 +107,13 @@ class MainWindow(QMainWindow):
QMessageBox.critical(self.parent(),
'Connecting to %s failed!' % host, str(e))
+ def on_validateCheckBox_toggled(self, state):
+ print "validateCheckBox_toggled", state
+
+ def on_visibilityComboBox_activated(self, level):
+ if level in ['user', 'admin', 'expert']:
+ print "visibility Level now:", level
+
def on_treeWidget_currentItemChanged(self, current, previous):
if current.type() == ITEM_TYPE_NODE:
self._displayNode(current.text(0))
diff --git a/secop/gui/modulectrl.py b/secop/gui/modulectrl.py
index 399b99b..fc2782f 100644
--- a/secop/gui/modulectrl.py
+++ b/secop/gui/modulectrl.py
@@ -56,6 +56,7 @@ class ParameterButtons(QWidget):
class ParameterGroup(QWidget):
+
def __init__(self, groupname, parent=None):
super(ParameterGroup, self).__init__(parent)
loadUi(self, 'paramgroup.ui')
@@ -107,19 +108,17 @@ class ModuleCtrl(QWidget):
self._node.newData.connect(self._updateValue)
-
def _initModuleWidgets(self):
initValues = self._node.queryCache(self._module)
row = 0
-
# collect grouping information
paramsByGroup = {} # groupname -> [paramnames]
allGroups = set()
params = self._node.getParameters(self._module)
for param in params:
props = self._node.getProperties(self._module, param)
- group = props.get('group',None)
+ group = props.get('group', None)
if group is not None:
allGroups.add(group)
paramsByGroup.setdefault(group, []).append(param)
@@ -139,7 +138,8 @@ class ModuleCtrl(QWidget):
# check if there is a param of the same name too
if group in params:
# yes: create a widget for this as well
- labelstr, buttons = self._makeEntry(param, initValues[param].value, nolabel=True, checkbox=checkbox, invert=True)
+ labelstr, buttons = self._makeEntry(
+ param, initValues[param].value, nolabel=True, checkbox=checkbox, invert=True)
checkbox.setText(labelstr)
# add to Layout (yes: ignore the label!)
@@ -153,7 +153,8 @@ class ModuleCtrl(QWidget):
for param in paramsByGroup[param]:
if param == group:
continue
- label, buttons = self._makeEntry(param, initValues[param].value, checkbox=checkbox, invert=False)
+ label, buttons = self._makeEntry(
+ param, initValues[param].value, checkbox=checkbox, invert=False)
# add to Layout
self.paramGroupBox.layout().addWidget(label, row, 0)
@@ -161,22 +162,29 @@ class ModuleCtrl(QWidget):
row += 1
else:
- # param is a 'normal' param: create a widget if it has no group or is named after a group (otherwise its created above)
+ # param is a 'normal' param: create a widget if it has no group
+ # or is named after a group (otherwise its created above)
props = self._node.getProperties(self._module, param)
if props.get('group', param) == param:
- label, buttons = self._makeEntry(param, initValues[param].value)
+ label, buttons = self._makeEntry(
+ param, initValues[param].value)
# add to Layout
self.paramGroupBox.layout().addWidget(label, row, 0)
self.paramGroupBox.layout().addWidget(buttons, row, 1)
row += 1
-
- def _makeEntry(self, param, initvalue, nolabel=False, checkbox=None, invert=False):
+ def _makeEntry(
+ self,
+ param,
+ initvalue,
+ nolabel=False,
+ checkbox=None,
+ invert=False):
props = self._node.getProperties(self._module, param)
description = props.get('description', '')
- unit = props.get('unit','')
+ unit = props.get('unit', '')
if unit:
labelstr = '%s (%s):' % (param, unit)
@@ -186,7 +194,8 @@ class ModuleCtrl(QWidget):
if checkbox and not invert:
labelstr = ' ' + labelstr
- buttons = ParameterButtons(self._module, param, initvalue, props['readonly'])
+ buttons = ParameterButtons(
+ self._module, param, initvalue, props['readonly'])
buttons.setRequested.connect(self._set_Button_pressed)
if description:
@@ -199,7 +208,11 @@ class ModuleCtrl(QWidget):
label.setFont(self._labelfont)
if checkbox:
- def stateChanged(newstate, buttons=buttons, label=None if nolabel else label, invert=invert):
+ def stateChanged(
+ newstate,
+ buttons=buttons,
+ label=None if nolabel else label,
+ invert=invert):
if (newstate and not invert) or (invert and not newstate):
buttons.show()
if label:
@@ -217,9 +230,6 @@ class ModuleCtrl(QWidget):
return label, buttons
-
-
-
def _set_Button_pressed(self, module, parameter, target):
sig = (module, parameter, target)
if self._lastclick == sig:
diff --git a/secop/gui/ui/mainwindow.ui b/secop/gui/ui/mainwindow.ui
index aea8b85..78e6e76 100644
--- a/secop/gui/ui/mainwindow.ui
+++ b/secop/gui/ui/mainwindow.ui
@@ -14,8 +14,8 @@
secop-gui
-
- -
+
+
-
Qt::Horizontal
@@ -23,7 +23,53 @@
-
-
+
+
+ 0
+
+
-
+
+
-
+
+ user
+
+
+ -
+
+ admin
+
+
+ -
+
+ expert
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+ Validate locally
+
+
+ true
+
+
+
+
-
@@ -58,7 +104,7 @@
0
0
1228
- 25
+ 23
+ -
+
+
+ Properties:
+
+
+
+ 0
+
+
+ 6
+
+
+ 0
+
+
+ 6
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
diff --git a/secop/gui/ui/parambuttons_select.ui b/secop/gui/ui/parambuttons_select.ui
new file mode 100644
index 0000000..cea746b
--- /dev/null
+++ b/secop/gui/ui/parambuttons_select.ui
@@ -0,0 +1,76 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 730
+ 33
+
+
+
+ Form
+
+
+
+ 6
+
+
+ 0
+
+
+ 0
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+ Current:
+
+
+
+ -
+
+
+ Set:
+
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+
+
+
diff --git a/secop/lib/__init__.py b/secop/lib/__init__.py
index f8d64f8..db7e128 100644
--- a/secop/lib/__init__.py
+++ b/secop/lib/__init__.py
@@ -21,7 +21,34 @@
# *****************************************************************************
"""Define helpers"""
+import os
+import re
+import sys
+import errno
+import signal
+import socket
+import fnmatch
+import linecache
import threading
+import traceback
+import subprocess
+import unicodedata
+from os import path
+
+
+class lazy_property(object):
+ """A property that calculates its value only once."""
+
+ def __init__(self, func):
+ self._func = func
+ self.__name__ = func.__name__
+ self.__doc__ = func.__doc__
+
+ def __get__(self, obj, obj_class):
+ if obj is None:
+ return obj
+ obj.__dict__[self.__name__] = self._func(obj)
+ return obj.__dict__[self.__name__]
class attrdict(dict):
@@ -48,8 +75,11 @@ def get_class(spec):
"""loads a class given by string in dotted notaion (as python would do)"""
modname, classname = spec.rsplit('.', 1)
import importlib
- module = importlib.import_module('secop.' + modname)
- # module = __import__(spec)
+ if modname.startswith('secop'):
+ module = importlib.import_module(modname)
+ else:
+ # rarely needed by now....
+ module = importlib.import_module('secop.' + modname)
return getattr(module, classname)
@@ -64,10 +94,6 @@ def mkthread(func, *args, **kwds):
return t
-import sys
-import linecache
-import traceback
-
def formatExtendedFrame(frame):
ret = []
for key, value in frame.f_locals.iteritems():
@@ -79,6 +105,7 @@ def formatExtendedFrame(frame):
ret.append('\n')
return ret
+
def formatExtendedTraceback(exc_info=None):
if exc_info is None:
etype, value, tb = sys.exc_info()
@@ -101,6 +128,7 @@ def formatExtendedTraceback(exc_info=None):
ret += traceback.format_exception_only(etype, value)
return ''.join(ret).rstrip('\n')
+
def formatExtendedStack(level=1):
f = sys._getframe(level)
ret = ['Stack trace (most recent call last):\n\n']
@@ -120,6 +148,7 @@ def formatExtendedStack(level=1):
f = f.f_back
return ''.join(ret).rstrip('\n')
+
def formatException(cut=0, exc_info=None, verbose=False):
"""Format an exception with traceback, but leave out the first `cut`
number of frames.
@@ -137,6 +166,63 @@ def formatException(cut=0, exc_info=None, verbose=False):
return ''.join(res)
+def parseHostPort(host, defaultport):
+ """Parse host[:port] string and tuples
+
+ Specify 'host[:port]' or a (host, port) tuple for the mandatory argument.
+ If the port specification is missing, the value of the defaultport is used.
+ """
+
+ if isinstance(host, (tuple, list)):
+ host, port = host
+ elif ':' in host:
+ host, port = host.rsplit(':', 1)
+ port = int(port)
+ else:
+ port = defaultport
+ assert 0 < port < 65536
+ assert ':' not in host
+ return host, port
+
+
+def tcpSocket(host, defaultport, timeout=None):
+ """Helper for opening a TCP client socket to a remote server.
+
+ Specify 'host[:port]' or a (host, port) tuple for the mandatory argument.
+ If the port specification is missing, the value of the defaultport is used.
+ If timeout is set to a number, the timout of the connection is set to this
+ number, else the socket stays in blocking mode.
+ """
+ host, port = parseHostPort(host, defaultport)
+
+ # open socket and set options
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ if timeout:
+ s.settimeout(timeout)
+ # connect
+ s.connect((host, int(port)))
+ return s
+
+
+def closeSocket(sock, socket=socket):
+ """Do our best to close a socket."""
+ if sock is None:
+ return
+ try:
+ sock.shutdown(socket.SHUT_RDWR)
+ except socket.error:
+ pass
+ try:
+ sock.close()
+ except socket.error:
+ pass
+
+
+def getfqdn(name=''):
+ """Get fully qualified hostname."""
+ return socket.getfqdn(name)
+
+
if __name__ == '__main__':
print "minimal testing: lib"
d = attrdict(a=1, b=2)
diff --git a/secop/devices/core.py b/secop/modules.py
similarity index 75%
rename from secop/devices/core.py
rename to secop/modules.py
index 5c61164..9249ef5 100644
--- a/secop/devices/core.py
+++ b/secop/modules.py
@@ -56,17 +56,15 @@ class PARAM(object):
unit=None,
readonly=True,
export=True,
- group=''):
- if isinstance(description, PARAM):
- # make a copy of a PARAM object
- self.__dict__.update(description.__dict__)
- return
+ group='',
+ poll=False):
if not isinstance(datatype, DataType):
if issubclass(datatype, DataType):
# goodie: make an instance from a class (forgotten ()???)
datatype = datatype()
else:
- raise ValueError('Datatype MUST be from datatypes!')
+ raise ValueError(
+ 'datatype MUST be derived from class DataType!')
self.description = description
self.datatype = datatype
self.default = default
@@ -74,6 +72,9 @@ class PARAM(object):
self.readonly = readonly
self.export = export
self.group = group
+ # note: auto-converts True/False to 1/0 which yield the expected
+ # behaviour...
+ self.poll = int(poll)
# internal caching: value and timestamp of last change...
self.value = default
self.timestamp = 0
@@ -82,6 +83,18 @@ class PARAM(object):
return '%s(%s)' % (self.__class__.__name__, ', '.join(
['%s=%r' % (k, v) for k, v in sorted(self.__dict__.items())]))
+ def copy(self):
+ # return a copy of ourselfs
+ return PARAM(description=self.description,
+ datatype=self.datatype,
+ default=self.default,
+ unit=self.unit,
+ readonly=self.readonly,
+ export=self.export,
+ group=self.group,
+ poll=self.poll,
+ )
+
def as_dict(self, static_only=False):
# used for serialisation only
res = dict(
@@ -99,11 +112,36 @@ class PARAM(object):
res['timestamp'] = format_time(self.timestamp)
return res
+ @property
+ def export_value(self):
+ return self.datatype.export(self.value)
+
+
+class OVERRIDE(object):
+
+ def __init__(self, **kwds):
+ self.kwds = kwds
+
+ def apply(self, paramobj):
+ if isinstance(paramobj, PARAM):
+ for k, v in self.kwds.iteritems():
+ if hasattr(paramobj, k):
+ setattr(paramobj, k, v)
+ return paramobj
+ else:
+ raise ProgrammingError(
+ "Can not apply Override(%s=%r) to %r: non-existing property!" %
+ (k, v, paramobj))
+ else:
+ raise ProgrammingError(
+ "Overrides can only be applied to PARAM's, %r is none!" %
+ paramobj)
+
# storage for CMDs settings (description + call signature...)
class CMD(object):
- def __init__(self, description, arguments, result):
+ def __init__(self, description, arguments=[], result=None):
# descriptive text for humans
self.description = description
# list of datatypes for arguments
@@ -122,10 +160,10 @@ class CMD(object):
arguments=map(export_datatype, self.arguments),
resulttype=export_datatype(self.resulttype), )
+
# Meta class
# warning: MAGIC!
-
class DeviceMeta(type):
def __new__(mcs, name, bases, attrs):
@@ -142,37 +180,61 @@ class DeviceMeta(type):
newentry.update(attrs.get(entry, {}))
setattr(newtype, entry, newentry)
+ # apply Overrides from all sub-classes
+ newparams = getattr(newtype, 'PARAMS')
+ for base in reversed(bases):
+ overrides = getattr(base, 'OVERRIDES', {})
+ for n, o in overrides.iteritems():
+ newparams[n] = o.apply(newparams[n].copy())
+ for n, o in attrs.get('OVERRIDES', {}).iteritems():
+ newparams[n] = o.apply(newparams[n].copy())
+
# check validity of PARAM entries
for pname, pobj in newtype.PARAMS.items():
# XXX: allow dicts for overriding certain aspects only.
if not isinstance(pobj, PARAM):
- raise ProgrammingError('%r: device PARAM %r should be a '
+ raise ProgrammingError('%r: PARAMs entry %r should be a '
'PARAM object!' % (name, pname))
+
# XXX: create getters for the units of params ??
+
# wrap of reading/writing funcs
rfunc = attrs.get('read_' + pname, None)
+ for base in bases:
+ if rfunc is not None:
+ break
+ rfunc = getattr(base, 'read_' + pname, None)
def wrapped_rfunc(self, maxage=0, pname=pname, rfunc=rfunc):
if rfunc:
+ self.log.debug("rfunc(%s): call %r" % (pname, rfunc))
value = rfunc(self, maxage)
setattr(self, pname, value)
return value
else:
# return cached value
+ self.log.debug("rfunc(%s): return cached value" % pname)
return self.PARAMS[pname].value
if rfunc:
wrapped_rfunc.__doc__ = rfunc.__doc__
- setattr(newtype, 'read_' + pname, wrapped_rfunc)
+ if getattr(rfunc, '__wrapped__', False) == False:
+ setattr(newtype, 'read_' + pname, wrapped_rfunc)
+ wrapped_rfunc.__wrapped__ = True
if not pobj.readonly:
wfunc = attrs.get('write_' + pname, None)
+ for base in bases:
+ if wfunc is not None:
+ break
+ wfunc = getattr(base, 'write_' + pname, None)
def wrapped_wfunc(self, value, pname=pname, wfunc=wfunc):
- self.log.debug("wfunc: set %s to %r" % (pname, value))
+ self.log.debug("wfunc(%s): set %r" % (pname, value))
pobj = self.PARAMS[pname]
- value = pobj.datatype.validate(value) if pobj.datatype else value
+ value = pobj.datatype.validate(value)
if wfunc:
+ self.log.debug('calling %r(%r)' % (wfunc, value))
value = wfunc(self, value) or value
# XXX: use setattr or direct manipulation
# of self.PARAMS[pname]?
@@ -181,14 +243,16 @@ class DeviceMeta(type):
if wfunc:
wrapped_wfunc.__doc__ = wfunc.__doc__
- setattr(newtype, 'write_' + pname, wrapped_wfunc)
+ if getattr(wfunc, '__wrapped__', False) == False:
+ setattr(newtype, 'write_' + pname, wrapped_wfunc)
+ wrapped_wfunc.__wrapped__ = True
def getter(self, pname=pname):
return self.PARAMS[pname].value
def setter(self, value, pname=pname):
pobj = self.PARAMS[pname]
- value = pobj.datatype.validate(value) if pobj.datatype else value
+ value = pobj.datatype.validate(value)
pobj.timestamp = time.time()
if not EVENT_ONLY_ON_CHANGED_VALUES or (value != pobj.value):
pobj.value = value
@@ -251,13 +315,7 @@ class Device(object):
# make local copies of PARAMS
params = {}
for k, v in self.PARAMS.items()[:]:
- #params[k] = PARAM(v)
- # PARAM: type(v) -> PARAM
- # type(v)(v) -> PARAM(v)
- # EPICS_PARAM: type(v) -> EPICS_PARAM
- # type(v)(v) -> EPICS_PARAM(v)
- param_type = type(v)
- params[k] = param_type(v)
+ params[k] = v.copy()
self.PARAMS = params
@@ -273,13 +331,13 @@ class Device(object):
mycls = self.__class__
myclassname = '%s.%s' % (mycls.__module__, mycls.__name__)
self.PROPERTIES['implementation'] = myclassname
- self.PROPERTIES['interfaces'] = [b.__name__ for b in mycls.__mro__
- if b.__module__.startswith('secop.devices.core')]
+ self.PROPERTIES['interfaces'] = [
+ b.__name__ for b in mycls.__mro__ if b.__module__.startswith('secop.modules')]
self.PROPERTIES['interface'] = self.PROPERTIES['interfaces'][0]
# remove unset (default) module properties
for k, v in self.PROPERTIES.items():
- if v == None:
+ if v is None:
del self.PROPERTIES[k]
# check and apply parameter_properties
@@ -312,13 +370,13 @@ class Device(object):
cfgdict[k] = v.default
# replace CLASS level PARAM objects with INSTANCE level ones
- #self.PARAMS[k] = PARAM(self.PARAMS[k])
- param_type = type(self.PARAMS[k])
- self.PARAMS[k] = param_type(self.PARAMS[k])
+ self.PARAMS[k] = self.PARAMS[k].copy()
# now 'apply' config:
# pass values through the datatypes and store as attributes
for k, v in cfgdict.items():
+ if k == 'value':
+ continue
# apply datatype, complain if type does not fit
datatype = self.PARAMS[k].datatype
if datatype is not None:
@@ -334,11 +392,7 @@ class Device(object):
def init(self):
# may be overriden in derived classes to init stuff
- self.log.debug('init()')
-
- def _pollThread(self):
- # may be overriden in derived classes to init stuff
- self.log.debug('init()')
+ self.log.debug('empty init()')
class Readable(Device):
@@ -348,7 +402,7 @@ class Readable(Device):
"""
PARAMS = {
'value': PARAM('current value of the device', readonly=True, default=0.,
- datatype=FloatRange()),
+ datatype=FloatRange(), poll=True),
'pollinterval': PARAM('sleeptime between polls', default=5,
readonly=False, datatype=FloatRange(0.1, 120), ),
'status': PARAM('current status of the device', default=(status.OK, ''),
@@ -360,25 +414,38 @@ class Readable(Device):
'UNSTABLE': status.UNSTABLE,
'ERROR': status.ERROR,
'UNKNOWN': status.UNKNOWN
- }), StringType() ),
- readonly=True),
+ }), StringType()),
+ readonly=True, poll=True),
}
def init(self):
Device.init(self)
- self._pollthread = threading.Thread(target=self._pollThread)
+ self._pollthread = threading.Thread(target=self.__pollThread)
self._pollthread.daemon = True
self._pollthread.start()
- def _pollThread(self):
+ def __pollThread(self):
"""super simple and super stupid per-module polling thread"""
+ i = 0
while True:
- time.sleep(self.pollinterval)
- for pname in self.PARAMS:
- if pname != 'pollinterval':
- rfunc = getattr(self, 'read_%s' % pname, None)
- if rfunc:
- rfunc()
+ i = 1
+ try:
+ time.sleep(self.pollinterval)
+ except TypeError:
+ time.sleep(max(self.pollinterval))
+ try:
+ self.poll(i)
+ except Exception: # really ALL
+ pass
+
+ def poll(self, nr):
+ for pname, pobj in self.PARAMS.iteritems():
+ if not pobj.poll:
+ continue
+ if 0 == nr % int(pobj.poll):
+ rfunc = getattr(self, 'read_' + pname, None)
+ if rfunc:
+ rfunc()
class Driveable(Readable):
@@ -387,9 +454,12 @@ class Driveable(Readable):
providing a settable 'target' parameter to those of a Readable
"""
PARAMS = {
- 'target': PARAM('target value of the device', default=0., readonly=False,
- datatype=FloatRange(),
- ),
+ 'target': PARAM(
+ 'target value of the device',
+ default=0.,
+ readonly=False,
+ datatype=FloatRange(),
+ ),
}
# XXX: CMDS ???? auto deriving working well enough?
diff --git a/secop/protocol/dispatcher.py b/secop/protocol/dispatcher.py
index adb9c46..9578074 100644
--- a/secop/protocol/dispatcher.py
+++ b/secop/protocol/dispatcher.py
@@ -96,8 +96,8 @@ class Dispatcher(object):
except Exception as err:
self.log.exception(err)
reply = msg.get_error(
- errorclass='InternalError',
- errorinfo=[formatException(), str(msg), formatExtendedStack()])
+ errorclass='InternalError', errorinfo=[
+ formatException(), str(msg), formatExtendedStack()])
else:
self.log.debug('Can not handle msg %r' % msg)
reply = self.unhandled(conn, msg)
@@ -125,7 +125,7 @@ class Dispatcher(object):
msg = Value(
moduleobj.name,
parameter=pname,
- value=pobj.value,
+ value=pobj.export_value,
t=pobj.timestamp)
self.broadcast_event(msg)
@@ -215,8 +215,9 @@ class Dispatcher(object):
for modulename in self._export:
module = self.get_module(modulename)
# some of these need rework !
- mod_desc = {'parameters':[], 'commands':[]}
- for pname, param in self.list_module_params(modulename, only_static=True).items():
+ mod_desc = {'parameters': [], 'commands': []}
+ for pname, param in self.list_module_params(
+ modulename, only_static=True).items():
mod_desc['parameters'].extend([pname, param])
for cname, cmd in self.list_module_cmds(modulename).items():
mod_desc['commands'].extend([cname, cmd])
@@ -236,7 +237,9 @@ class Dispatcher(object):
module = self.get_module(modulename)
# some of these need rework !
dd = {
- 'parameters': self.list_module_params(modulename, only_static=True),
+ 'parameters': self.list_module_params(
+ modulename,
+ only_static=True),
'commands': self.list_module_cmds(modulename),
'properties': module.PROPERTIES,
}
@@ -319,9 +322,9 @@ class Dispatcher(object):
return Value(
modulename,
parameter=pname,
- value=pobj.value,
+ value=pobj.export_value,
t=pobj.timestamp)
- return Value(modulename, parameter=pname, value=pobj.value)
+ return Value(modulename, parameter=pname, value=pobj.export_value)
# now the (defined) handlers for the different requests
def handle_Help(self, conn, msg):
@@ -396,7 +399,7 @@ class Dispatcher(object):
res = Value(
module=modulename,
parameter=pname,
- value=pobj.value,
+ value=pobj.export_value,
t=pobj.timestamp,
unit=pobj.unit)
if res.value != Ellipsis: # means we do not have a value at all so skip this
diff --git a/secop/devices/__init__.py b/secop_demo/__init__.py
similarity index 100%
rename from secop/devices/__init__.py
rename to secop_demo/__init__.py
diff --git a/secop/devices/cryo.py b/secop_demo/cryo.py
similarity index 98%
rename from secop/devices/cryo.py
rename to secop_demo/cryo.py
index 790ecf7..47b36b6 100644
--- a/secop/devices/cryo.py
+++ b/secop_demo/cryo.py
@@ -25,7 +25,7 @@ import time
import random
import threading
-from secop.devices.core import Driveable, CMD, PARAM
+from secop.modules import Driveable, CMD, PARAM
from secop.protocol import status
from secop.datatypes import FloatRange, EnumType, TupleOf
from secop.lib import clamp, mkthread
@@ -66,15 +66,15 @@ class Cryostat(CryoBase):
datatype=FloatRange(0), default=1, unit="W",
readonly=False,
group='heater_settings',
- ),
+ ),
heater=PARAM("current heater setting",
datatype=FloatRange(0, 100), default=0, unit="%",
group='heater_settings',
- ),
+ ),
heaterpower=PARAM("current heater power",
datatype=FloatRange(0), default=0, unit="W",
group='heater_settings',
- ),
+ ),
target=PARAM("target temperature",
datatype=FloatRange(0), default=0, unit="K",
readonly=False,
@@ -112,21 +112,23 @@ class Cryostat(CryoBase):
datatype=FloatRange(0, 100), default=0.1, unit='K',
readonly=False,
group='stability',
- ),
+ ),
window=PARAM("time window for stability checking",
datatype=FloatRange(1, 900), default=30, unit='s',
readonly=False,
group='stability',
- ),
+ ),
timeout=PARAM("max waiting time for stabilisation check",
datatype=FloatRange(1, 36000), default=900, unit='s',
readonly=False,
group='stability',
- ),
+ ),
)
CMDS = dict(
- Stop=CMD("Stop ramping the setpoint\n\nby setting the current setpoint as new target",
- [], None),
+ Stop=CMD(
+ "Stop ramping the setpoint\n\nby setting the current setpoint as new target",
+ [],
+ None),
)
def init(self):
diff --git a/secop/devices/demo.py b/secop_demo/modules.py
similarity index 91%
rename from secop/devices/demo.py
rename to secop_demo/modules.py
index da2d499..b565525 100644
--- a/secop/devices/demo.py
+++ b/secop_demo/modules.py
@@ -24,7 +24,7 @@ import time
import random
import threading
-from secop.devices.core import Readable, Driveable, PARAM
+from secop.modules import Readable, Driveable, PARAM
from secop.datatypes import EnumType, FloatRange, IntRange, ArrayOf, StringType, TupleOf, StructOf, BoolType
from secop.protocol import status
@@ -287,18 +287,18 @@ class DatatypesTest(Readable):
"""
"""
PARAMS = {
- 'enum': PARAM('enum',
- datatype=EnumType('boo', 'faar', z=9), readonly=False, default=1),
- 'tupleof': PARAM('tuple of int, float and str',
- datatype=TupleOf(IntRange(), FloatRange(), StringType()), readonly=False, default=(1, 2.3, 'a')),
- 'arrayof': PARAM('array: 2..3 times bool',
- datatype=ArrayOf(BoolType(), 2, 3), readonly=False, default=[1, 0, 1]),
- 'intrange': PARAM('intrange',
- datatype=IntRange(2, 9), readonly=False, default=4),
- 'floatrange': PARAM('floatrange',
- datatype=FloatRange(-1, 1), readonly=False, default=0,
- ),
- 'struct': PARAM('struct(a=str, b=int, c=bool)',
- datatype=StructOf(a=StringType(), b=IntRange(), c=BoolType()),
- ),
- }
+ 'enum': PARAM(
+ 'enum', datatype=EnumType(
+ 'boo', 'faar', z=9), readonly=False, default=1), 'tupleof': PARAM(
+ 'tuple of int, float and str', datatype=TupleOf(
+ IntRange(), FloatRange(), StringType()), readonly=False, default=(
+ 1, 2.3, 'a')), 'arrayof': PARAM(
+ 'array: 2..3 times bool', datatype=ArrayOf(
+ BoolType(), 2, 3), readonly=False, default=[
+ 1, 0, 1]), 'intrange': PARAM(
+ 'intrange', datatype=IntRange(
+ 2, 9), readonly=False, default=4), 'floatrange': PARAM(
+ 'floatrange', datatype=FloatRange(
+ -1, 1), readonly=False, default=0, ), 'struct': PARAM(
+ 'struct(a=str, b=int, c=bool)', datatype=StructOf(
+ a=StringType(), b=IntRange(), c=BoolType()), ), }
diff --git a/secop/devices/test.py b/secop_demo/test.py
similarity index 83%
rename from secop/devices/test.py
rename to secop_demo/test.py
index 30278b4..0ff6ccc 100644
--- a/secop/devices/test.py
+++ b/secop_demo/test.py
@@ -22,9 +22,10 @@
import random
-from secop.devices.core import Readable, Driveable, PARAM
+from secop.modules import Readable, Driveable, PARAM
from secop.datatypes import FloatRange, StringType
+
class LN2(Readable):
"""Just a readable.
@@ -62,12 +63,20 @@ class Temp(Driveable):
but the implementation may do anything
"""
PARAMS = {
- 'sensor': PARAM("Sensor number or calibration id",
- datatype=StringType(8,16), readonly=True,
- ),
- 'target': PARAM("Target temperature",
- default=300.0, datatype=FloatRange(0), readonly=False, unit='K',
- ),
+ 'sensor': PARAM(
+ "Sensor number or calibration id",
+ datatype=StringType(
+ 8,
+ 16),
+ readonly=True,
+ ),
+ 'target': PARAM(
+ "Target temperature",
+ default=300.0,
+ datatype=FloatRange(0),
+ readonly=False,
+ unit='K',
+ ),
}
def read_value(self, maxage=0):
diff --git a/secop_ess/__init__.py b/secop_ess/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/secop/devices/epics.py b/secop_ess/epics.py
similarity index 80%
rename from secop/devices/epics.py
rename to secop_ess/epics.py
index 4c1bd46..e73c444 100644
--- a/secop/devices/epics.py
+++ b/secop_ess/epics.py
@@ -24,7 +24,7 @@ import random
from secop.lib.parsing import format_time
from secop.datatypes import EnumType, TupleOf, FloatRange, get_datatype, StringType
-from secop.devices.core import Readable, Device, Driveable, PARAM
+from secop.modules import Readable, Device, Driveable, PARAM
from secop.protocol import status
try:
@@ -62,18 +62,18 @@ class EpicsReadable(Readable):
"""EpicsDriveable handles a Driveable interfacing to EPICS v4"""
# Commmon PARAMS for all EPICS devices
PARAMS = {
- 'value': PARAM('EPICS generic value',
- datatype=FloatRange(),
- default=300.0,),
+ 'value': PARAM('EPICS generic value',
+ datatype=FloatRange(),
+ default=300.0,),
'epics_version': PARAM("EPICS version used, v3 or v4",
datatype=EnumType(v3=3, v4=4),),
# 'private' parameters: not remotely accessible
- 'value_pv': PARAM('EPICS pv_name of value',
- datatype=StringType(),
- default="unset", export=False),
- 'status_pv': PARAM('EPICS pv_name of status',
- datatype=StringType(),
- default="unset", export=False),
+ 'value_pv': PARAM('EPICS pv_name of value',
+ datatype=StringType(),
+ default="unset", export=False),
+ 'status_pv': PARAM('EPICS pv_name of status',
+ datatype=StringType(),
+ default="unset", export=False),
}
# Generic read and write functions
@@ -122,19 +122,19 @@ class EpicsDriveable(Driveable):
"""EpicsDriveable handles a Driveable interfacing to EPICS v4"""
# Commmon PARAMS for all EPICS devices
PARAMS = {
- 'target': PARAM('EPICS generic target', datatype=FloatRange(),
- default=300.0, readonly=False),
- 'value': PARAM('EPICS generic value', datatype=FloatRange(),
- default=300.0,),
+ 'target': PARAM('EPICS generic target', datatype=FloatRange(),
+ default=300.0, readonly=False),
+ 'value': PARAM('EPICS generic value', datatype=FloatRange(),
+ default=300.0,),
'epics_version': PARAM("EPICS version used, v3 or v4",
datatype=StringType(),),
# 'private' parameters: not remotely accessible
- 'target_pv': PARAM('EPICS pv_name of target', datatype=StringType(),
- default="unset", export=False),
- 'value_pv': PARAM('EPICS pv_name of value', datatype=StringType(),
- default="unset", export=False),
- 'status_pv': PARAM('EPICS pv_name of status', datatype=StringType(),
- default="unset", export=False),
+ 'target_pv': PARAM('EPICS pv_name of target', datatype=StringType(),
+ default="unset", export=False),
+ 'value_pv': PARAM('EPICS pv_name of value', datatype=StringType(),
+ default="unset", export=False),
+ 'status_pv': PARAM('EPICS pv_name of status', datatype=StringType(),
+ default="unset", export=False),
}
# Generic read and write functions
@@ -182,8 +182,11 @@ class EpicsDriveable(Driveable):
# XXX: how to map an unknown type+value to an valid status ???
return status.UNKNOWN, self._read_pv(self.status_pv)
# status_pv is unset, derive status from equality of value + target
- return (status.OK, '') if self.read_value() == self.read_target() else \
- (status.BUSY, 'Moving')
+ return (
+ status.OK,
+ '') if self.read_value() == self.read_target() else (
+ status.BUSY,
+ 'Moving')
"""Temperature control loop"""
@@ -195,11 +198,11 @@ class EpicsTempCtrl(EpicsDriveable):
PARAMS = {
# TODO: restrict possible values with oneof datatype
- 'heaterrange': PARAM('Heater range', datatype=StringType(),
- default='Off', readonly=False,),
- 'tolerance': PARAM('allowed deviation between value and target',
- datatype=FloatRange(1e-6, 1e6), default=0.1,
- readonly=False,),
+ 'heaterrange': PARAM('Heater range', datatype=StringType(),
+ default='Off', readonly=False,),
+ 'tolerance': PARAM('allowed deviation between value and target',
+ datatype=FloatRange(1e-6, 1e6), default=0.1,
+ readonly=False,),
# 'private' parameters: not remotely accessible
'heaterrange_pv': PARAM('EPICS pv_name of heater range',
datatype=StringType(), default="unset", export=False,),
@@ -221,7 +224,11 @@ class EpicsTempCtrl(EpicsDriveable):
# XXX: comparison may need to collect a history to detect oscillations
at_target = abs(self.read_value(maxage) - self.read_target(maxage)) \
<= self.tolerance
- return (status.OK, 'at Target') if at_target else (status.BUSY, 'Moving')
+ return (
+ status.OK,
+ 'at Target') if at_target else (
+ status.BUSY,
+ 'Moving')
# TODO: add support for strings over epics pv
# def read_heaterrange(self, maxage=0):
diff --git a/secop_mlz/__init__.py b/secop_mlz/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/secop_mlz/entangle.py b/secop_mlz/entangle.py
new file mode 100644
index 0000000..30f362b
--- /dev/null
+++ b/secop_mlz/entangle.py
@@ -0,0 +1,1024 @@
+# -*- 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:
+# Alexander Lenz
+# Enrico Faulhaber
+#
+# *****************************************************************************
+
+# This is based upon the entangle-nicos integration
+"""
+This module contains the MLZ SECoP - TANGO integration.
+
+Here we support devices which fulfill the official
+MLZ TANGO interface for the respective device classes.
+"""
+
+import re
+import sys
+from time import sleep, time as currenttime
+import threading
+
+import PyTango
+import numpy
+
+from secop.lib import lazy_property, mkthread
+from secop.protocol import status
+from secop.datatypes import *
+from secop.errors import SECoPServerError, ConfigError, ProgrammingError, CommunicationError, HardwareError
+from secop.modules import PARAM, CMD, OVERRIDE, Device, Readable, Driveable
+
+
+# Only export these classes for 'from secop_mlz import *'
+__all__ = [
+ 'AnalogInput', 'Sensor',
+ 'AnalogOutput', 'Actuator', 'Motor',
+ 'TemperatureController', 'PowerSupply',
+ 'DigitalInput', 'NamedDigitalInput', 'PartialDigitalInput',
+ 'DigitalOutput', 'NamedDigitalOutput', 'PartialDigitalOutput',
+ 'StringIO',
+]
+
+EXC_MAPPING = {
+ PyTango.CommunicationFailed: CommunicationError,
+ PyTango.WrongNameSyntax: ConfigError,
+ PyTango.DevFailed: HardwareError,
+}
+
+REASON_MAPPING = {
+ 'Entangle_ConfigurationError': ConfigError,
+ 'Entangle_WrongAPICall': ProgrammingError,
+ 'Entangle_CommunicationFailure': CommunicationError,
+ 'Entangle_InvalidValue': ValueError,
+ 'Entangle_ProgrammingError': ProgrammingError,
+ 'Entangle_HardwareFailure': HardwareError,
+}
+
+# Tango DevFailed reasons that should not cause a retry
+FATAL_REASONS = set((
+ 'Entangle_ConfigurationError',
+ 'Entangle_UnrecognizedHardware',
+ 'Entangle_WrongAPICall',
+ 'Entangle_InvalidValue',
+ 'Entangle_NotSupported',
+ 'Entangle_ProgrammingError',
+ 'DB_DeviceNotDefined',
+ 'API_DeviceNotDefined',
+ 'API_CantConnectToDatabase',
+ 'API_TangoHostNotSet',
+ 'API_ServerNotRunning',
+ 'API_DeviceNotExported',
+))
+
+
+def describe_dev_error(exc):
+ """Return a better description for a Tango exception.
+
+ Most Tango exceptions are quite verbose and not suitable for user
+ consumption. Map the most common ones, that can also happen during normal
+ operation, to a bit more friendly ones.
+ """
+ # general attributes
+ reason = exc.reason.strip()
+ fulldesc = reason + ': ' + exc.desc.strip()
+ # reduce Python tracebacks
+ if '\n' in exc.origin and 'File ' in exc.origin:
+ origin = exc.origin.splitlines()[-2].strip()
+ else:
+ origin = exc.origin.strip()
+
+ # we don't need origin info for Tango itself
+ if origin.startswith(('DeviceProxy::', 'DeviceImpl::', 'Device_3Impl::',
+ 'Device_4Impl::', 'Connection::', 'TangoMonitor::')):
+ origin = None
+
+ # now handle specific cases better
+ if reason == 'API_AttrNotAllowed':
+ m = re.search(r'to (read|write) attribute (\w+)', fulldesc)
+ if m:
+ if m.group(1) == 'read':
+ fulldesc = 'reading %r not allowed in current state'
+ else:
+ fulldesc = 'writing %r not allowed in current state'
+ fulldesc %= m.group(2)
+ elif reason == 'API_CommandNotAllowed':
+ m = re.search(r'Command (\w+) not allowed when the '
+ r'device is in (\w+) state', fulldesc)
+ if m:
+ fulldesc = 'executing %r not allowed in state %s' \
+ % (m.group(1), m.group(2))
+ elif reason == 'API_DeviceNotExported':
+ m = re.search(r'Device ([\w/]+) is not', fulldesc)
+ if m:
+ fulldesc = 'Tango device %s is not exported, is the server ' \
+ 'running?' % m.group(1)
+ elif reason == 'API_CorbaException':
+ if 'TRANSIENT_CallTimedout' in fulldesc:
+ fulldesc = 'Tango client-server call timed out'
+ elif 'TRANSIENT_ConnectFailed' in fulldesc:
+ fulldesc = 'connection to Tango server failed, is the server ' \
+ 'running?'
+ elif reason == 'API_CantConnectToDevice':
+ m = re.search(r'connect to device ([\w/]+)', fulldesc)
+ if m:
+ fulldesc = 'connection to Tango device %s failed, is the server ' \
+ 'running?' % m.group(1)
+ elif reason == 'API_CommandTimedOut':
+ if 'acquire serialization' in fulldesc:
+ fulldesc = 'Tango call timed out waiting for lock on server'
+
+ # append origin if wanted
+ if origin:
+ fulldesc += ' in %s' % origin
+ return fulldesc
+
+
+class PyTangoDevice(Device):
+ """
+ Basic PyTango device.
+
+ The PyTangoDevice uses an internal PyTango.DeviceProxy but wraps command
+ execution and attribute operations with logging and exception mapping.
+ """
+
+ PARAMS = {
+ 'comtries': PARAM('Maximum retries for communication',
+ datatype=IntRange(1, 100), default=3, readonly=False, group='communication'),
+ 'comdelay': PARAM('Delay between retries', datatype=FloatRange(0), unit='s', default=0.1,
+ readonly=False, group='communication'),
+
+ 'tangodevice': PARAM('Tango device name',
+ datatype=StringType(), readonly=True,
+# export=True, # for testing only
+ export=False,
+ ),
+ }
+
+ tango_status_mapping = {
+ PyTango.DevState.ON: status.OK,
+ PyTango.DevState.ALARM: status.WARN,
+ PyTango.DevState.OFF: status.ERROR,
+ PyTango.DevState.FAULT: status.ERROR,
+ PyTango.DevState.MOVING: status.BUSY,
+ }
+
+ @lazy_property
+ def _com_lock(self):
+ return threading.Lock()
+
+ def _com_retry(self, info, function, *args, **kwds):
+ """Try communicating with the hardware/device.
+
+ PARAMeter "info" is passed to _com_return and _com_raise methods that
+ process the return value or exception raised after maximum tries.
+ """
+ tries = self.comtries
+ with self._com_lock:
+ while True:
+ tries -= 1
+ try:
+ result = function(*args, **kwds)
+ return self._com_return(result, info)
+ except Exception as err:
+ if tries == 0:
+ self._com_raise(err, info)
+ else:
+ name = getattr(function, '__name__', 'communication')
+ self._com_warn(tries, name, err, info)
+ sleep(self.comdelay)
+
+ def init(self):
+ # Wrap PyTango client creation (so even for the ctor, logging and
+ # exception mapping is enabled).
+ self._createPyTangoDevice = self._applyGuardToFunc(
+ self._createPyTangoDevice, 'constructor')
+ super(PyTangoDevice, self).init()
+
+ @lazy_property
+ def _dev(self):
+ return self._createPyTangoDevice(self.tangodevice)
+
+ def _hw_wait(self):
+ """Wait until hardware status is not BUSY."""
+ while PyTangoDevice.doStatus(self, 0)[0] == status.BUSY:
+ sleep(self._base_loop_delay)
+
+ def _getProperty(self, name, dev=None):
+ """
+ Utility function for getting a property by name easily.
+ """
+ if dev is None:
+ dev = self._dev
+ # Entangle and later API
+ if dev.command_query('GetProperties').in_type == PyTango.DevVoid:
+ props = dev.GetProperties()
+ return props[props.index(name) + 1] if name in props else None
+ # old (pre-Entangle) API
+ return dev.GetProperties([name, 'device'])[2]
+
+ def _createPyTangoDevice(self, address): # pylint: disable=E0202
+ """
+ Creates the PyTango DeviceProxy and wraps command execution and
+ attribute operations with logging and exception mapping.
+ """
+ device = PyTango.DeviceProxy(address)
+ # detect not running and not exported devices early, because that
+ # otherwise would lead to attribute errors later
+ try:
+ device.State
+ except AttributeError:
+ raise CommunicationError(
+ self, 'connection to Tango server failed, '
+ 'is the server running?')
+ return self._applyGuardsToPyTangoDevice(device)
+
+ def _applyGuardsToPyTangoDevice(self, dev):
+ """
+ Wraps command execution and attribute operations of the given
+ device with logging and exception mapping.
+ """
+ dev.command_inout = self._applyGuardToFunc(dev.command_inout)
+ dev.write_attribute = self._applyGuardToFunc(dev.write_attribute,
+ 'attr_write')
+ dev.read_attribute = self._applyGuardToFunc(dev.read_attribute,
+ 'attr_read')
+ dev.attribute_query = self._applyGuardToFunc(dev.attribute_query,
+ 'attr_query')
+ return dev
+
+ def _applyGuardToFunc(self, func, category='cmd'):
+ """
+ Wrap given function with logging and exception mapping.
+ """
+ def wrap(*args, **kwds):
+ # handle different types for better debug output
+ if category == 'cmd':
+ self.log.debug('[PyTango] command: %s%r', args[0], args[1:])
+ elif category == 'attr_read':
+ self.log.debug('[PyTango] read attribute: %s', args[0])
+ elif category == 'attr_write':
+ self.log.debug('[PyTango] write attribute: %s => %r',
+ args[0], args[1:])
+ elif category == 'attr_query':
+ self.log.debug('[PyTango] query attribute properties: %s',
+ args[0])
+ elif category == 'constructor':
+ self.log.debug('[PyTango] device creation: %s', args[0])
+ elif category == 'internal':
+ self.log.debug('[PyTango integration] internal: %s%r',
+ func.__name__, args)
+ else:
+ self.log.debug('[PyTango] call: %s%r', func.__name__, args)
+
+ info = category + ' ' + args[0] if args else category
+ return self._com_retry(info, func, *args, **kwds)
+
+ # hide the wrapping
+ wrap.__name__ = func.__name__
+
+ return wrap
+
+ def _com_return(self, result, info):
+ """Process *result*, the return value of communication.
+
+ Can raise an exception to initiate a retry. Default is to return
+ result unchanged.
+ """
+ # XXX: explicit check for loglevel to avoid expensive reprs
+ if isinstance(result, PyTango.DeviceAttribute):
+ the_repr = repr(result.value)[:300]
+ else:
+ # This line explicitly logs '=> None' for commands which
+ # does not return a value. This indicates that the command
+ # execution ended.
+ the_repr = repr(result)[:300]
+ self.log.debug('\t=> %s', the_repr)
+ return result
+
+ def _tango_exc_desc(self, err):
+ exc = str(err)
+ if err.args:
+ exc = err.args[0] # Can be str or DevError
+ if isinstance(exc, PyTango.DevError):
+ return describe_dev_error(exc)
+ return exc
+
+ def _tango_exc_reason(self, err):
+ if err.args and isinstance(err.args[0], PyTango.DevError):
+ return err.args[0].reason.strip()
+ return ''
+
+ def _com_warn(self, retries, name, err, info):
+ """Gives the opportunity to warn the user on failed tries.
+
+ Can also call _com_raise to abort early.
+ """
+ if self._tango_exc_reason(err) in FATAL_REASONS:
+ self._com_raise(err, info)
+ if retries == self.comtries - 1:
+ self.log.warning('%s failed, retrying up to %d times: %s',
+ info, retries, self._tango_exc_desc(err))
+
+ def _com_raise(self, err, info):
+ """Process the exception raised either by communication or _com_return.
+
+ Should raise a NICOS exception. Default is to raise
+ CommunicationError.
+ """
+ reason = self._tango_exc_reason(err)
+ exclass = REASON_MAPPING.get(
+ reason, EXC_MAPPING.get(type(err), CommunicationError))
+ fulldesc = self._tango_exc_desc(err)
+ self.log.debug('PyTango error: %s', fulldesc)
+ raise exclass(self, fulldesc)
+
+ def read_status(self, maxage=0):
+ # Query status code and string
+ tangoState = self._dev.State()
+ tangoStatus = self._dev.Status()
+
+ # Map status
+ myState = self.tango_status_mapping.get(tangoState, status.UNKNOWN)
+
+ return (myState, tangoStatus)
+
+ def do_reset(self):
+ self._dev.Reset()
+
+
+class AnalogInput(PyTangoDevice, Readable):
+ """
+ The AnalogInput handles all devices only delivering an analogue value.
+ """
+
+ def init(self):
+ super(AnalogInput, self).init()
+ # query unit from tango and update value property
+ attrInfo = self._dev.attribute_query('value')
+ # prefer configured unit if nothing is set on the Tango device, else
+ # update
+ if attrInfo.unit != 'No unit':
+ self.PARAMS['value'].unit = attrInfo.unit
+
+ def read_value(self, maxage=0):
+ return self._dev.value
+
+
+class Sensor(AnalogInput):
+ """
+ The sensor interface describes all analog read only devices.
+
+ The difference to AnalogInput is that the “value” attribute can be
+ converted from the “raw value” to a physical value with an offset and a
+ formula.
+ """
+ # note: we don't transport the formula to secop....
+ # we support the adjust method
+
+ def do_setposition(self, value):
+ self._dev.Adjust(value)
+
+
+class AnalogOutput(PyTangoDevice, Driveable):
+ """
+ The AnalogOutput handles all devices which set an analogue value.
+
+ The main application field is the output of any signal which may be
+ considered as continously in a range. The values may have nearly any
+ value between the limits. The compactness is limited by the resolution of
+ the hardware.
+
+ This class should be considered as a base class for motors, temperature
+ controllers, ...
+ """
+
+ PARAMS = {
+ 'userlimits': PARAM(
+ 'User defined limits of device value',
+ unit='main',
+ datatype=TupleOf(
+ FloatRange(),
+ FloatRange()),
+ default=(
+ float('-Inf'),
+ float('+Inf')),
+ readonly=False,
+ poll=10),
+ 'abslimits': PARAM(
+ 'Absolute limits of device value',
+ unit='main',
+ datatype=TupleOf(
+ FloatRange(),
+ FloatRange()),
+ ),
+ 'precision': PARAM(
+ 'Precision of the device value (allowed deviation '
+ 'of stable values from target)',
+ unit='main',
+ datatype=FloatRange(1e-38),
+ readonly=False,
+ ),
+ 'window': PARAM(
+ 'Time window for checking stabilization if > 0',
+ unit='s',
+ default=60.0,
+ datatype=FloatRange(
+ 0,
+ 900),
+ readonly=False,
+ ),
+ 'pollinterval': PARAM(
+ '[min, max] sleeptime between polls',
+ default=[
+ 0.5,
+ 5],
+ readonly=False,
+ datatype=TupleOf(
+ FloatRange(
+ 0,
+ 20),
+ FloatRange(
+ 0.1,
+ 120)),
+ ),
+ }
+ OVERRIDES = {
+ 'value': OVERRIDE(poll=False),
+ }
+
+ def init(self):
+ super(AnalogInput, self).init()
+ # query unit from tango and update value property
+ attrInfo = self._dev.attribute_query('value')
+ # prefer configured unit if nothing is set on the Tango device, else
+ # update
+ if attrInfo.unit != 'No unit':
+ self.PARAMS['value'].unit = attrInfo.unit
+
+ # init history
+ self._history = [] # will keep (timestamp, value) tuple
+ mkthread(self._history_thread)
+
+ def _history_thread(self):
+ while True:
+ # adaptive sleeping interval
+ if self.status[0] == status.BUSY:
+ sleep(min(self.pollinterval))
+ else:
+ sleep(min(max(self.pollinterval) / 2.,
+ max(self.window / 10., min(pollinterval))))
+ try:
+ self.read_value(0) # also append to self._history
+ # shorten history
+ while len(self._history) > 2:
+ # if history would be too short, break
+ if self._history[-1][0] - \
+ self._history[1][0] < self.window:
+ break
+ # remove a stale point
+ self._history.pop(0)
+ except Exception:
+ pass
+
+ def read_value(self, maxage=0):
+ value = self._dev.value
+ self._history.append((currenttime(), value))
+ return value
+
+ def read_target(self, maxage=0):
+ attrObj = self._dev.read_attribute('value')
+ return attrObj.w_value
+
+ def _isAtTarget(self):
+ if self.target is None:
+ return True # avoid bootstrapping problems
+ # check subset of _history which is in window
+ # also check if there is at least one value before window
+ # to know we have enough datapoints
+ hist = self._history[:]
+ window_start = currenttime() - self.window
+ hist_in_window = [v for (t, v) in hist if t >= window_start]
+ stable = all(abs(v - self.target) <= self.precision
+ for v in hist_in_window)
+ return 0 < len(hist_in_window) < len(hist) and stable
+
+ @property
+ def absmin(self):
+ return self.abslimits[0]
+
+ @property
+ def absmax(self):
+ return self.abslimits[1]
+
+ def __getusermin(self):
+ return self.userlimits[0]
+
+ def __setusermin(self, value):
+ self.userlimits = (value, self.userlimits[1])
+
+ usermin = property(__getusermin, __setusermin)
+
+ def __getusermax(self):
+ return self.userlimits[1]
+
+ def __setusermax(self, value):
+ self.userlimits = (self.userlimits[0], value)
+
+ usermax = property(__getusermax, __setusermax)
+
+ del __getusermin, __setusermin, __getusermax, __setusermax
+
+ def _checkLimits(self, limits):
+ umin, umax = limits
+ amin, amax = self.abslimits
+ if umin > umax:
+ raise ValueError(
+ self, 'user minimum (%s) above the user '
+ 'maximum (%s)' % (umin, umax))
+ if umin < amin - abs(amin * 1e-12):
+ umin = amin
+ if umax > amax + abs(amax * 1e-12):
+ umax = amax
+ return (umin, umax)
+
+ def write_userlimits(self, value):
+ return self._checkLimits(value)
+
+ def do_start(self, value=FloatRange()):
+ try:
+ self._dev.value = value
+ except HardwareError:
+ # changing target value during movement is not allowed by the
+ # Tango base class state machine. If we are moving, stop first.
+ if self.read_status(0)[0] == status.BUSY:
+ self.stop()
+ self._hw_wait()
+ self._dev.value = value
+ else:
+ raise
+
+ def do_stop(self):
+ self._dev.Stop()
+
+
+class Actuator(AnalogOutput):
+ """
+ The actuator interface describes all analog devices which DO something in a
+ defined way.
+
+ The difference to AnalogOutput is that there is a speed attribute, and the
+ value attribute is converted from the “raw value” with a formula and
+ offset.
+ """
+ # for secop: support the speed and ramp parameters
+
+ PARAMS = {
+ 'speed': PARAM(
+ 'The speed of changing the value',
+ unit='main/s',
+ readonly=False,
+ datatype=FloatRange(0)),
+ 'ramp': PARAM(
+ 'The speed of changing the value',
+ unit='main/min',
+ readonly=False,
+ datatype=FloatRange(0),
+ poll=30),
+ }
+
+ def read_speed(self):
+ return self._dev.speed
+
+ def write_speed(self, value):
+ self._dev.speed = value
+
+ def read_ramp(self):
+ return self.read_speed() * 60
+
+ def write_ramp(self, value):
+ self.write_speed(value / 60.)
+ return self.speed * 60
+
+ def do_setposition(self, value):
+ self._dev.Adjust(value)
+
+
+class Motor(Actuator):
+ """
+ This class implements a motor device (in a sense of a real motor
+ (stepper motor, servo motor, ...)).
+
+ It has the ability to move a real object from one place to another place.
+ """
+
+ PARAMS = {
+ 'refpos': PARAM(
+ 'Reference position',
+ datatype=FloatRange(),
+ unit='main'),
+ 'accel': PARAM(
+ 'Acceleration',
+ datatype=FloatRange(),
+ readonly=False,
+ unit='main/s^2'),
+ 'decel': PARAM(
+ 'Deceleration',
+ datatype=FloatRange(),
+ readonly=False,
+ unit='main/s^2'),
+ }
+
+ def read_refpos(self):
+ return float(self._getProperty('refpos'))
+
+ def read_accel(self):
+ return self._dev.accel
+
+ def write_accel(self, value):
+ self._dev.accel = value
+
+ def read_decel(self):
+ return self._dev.decel
+
+ def write_decel(self, value):
+ self._dev.decel = value
+
+ def do_reference(self):
+ self._dev.Reference()
+ return self.read_value()
+
+
+class TemperatureController(Actuator):
+ """
+ A temperature control loop device.
+ """
+
+ PARAMS = {
+ 'p': PARAM('Proportional control PARAMeter', datatype=FloatRange(),
+ readonly=False, group='pid',
+ ),
+ 'i': PARAM('Integral control PARAMeter', datatype=FloatRange(),
+ readonly=False, group='pid',
+ ),
+ 'd': PARAM('Derivative control PARAMeter', datatype=FloatRange(),
+ readonly=False, group='pid',
+ ),
+ 'pid': PARAM('pid control PARAMeters', datatype=TupleOf(FloatRange(), FloatRange(), FloatRange()),
+ readonly=False, group='pid', poll=30,
+ ),
+ 'setpoint': PARAM('Current setpoint', datatype=FloatRange(), poll=1,
+ ),
+ 'heateroutput': PARAM('Heater output', datatype=FloatRange(), poll=1,
+ ),
+ 'ramp': PARAM('Temperature ramp', unit='main/min',
+ datatype=FloatRange(), readonly=False, poll=30),
+ }
+
+ OVERRIDES = {
+ # We want this to be freely user-settable, and not produce a warning
+ # on startup, so select a usually sensible default.
+ 'precision': OVERRIDE(default=0.1),
+ }
+
+ def read_ramp(self):
+ return self._dev.ramp
+
+ def write_ramp(self, value):
+ self._dev.ramp = value
+ return self._dev.ramp
+
+ def read_p(self):
+ return self._dev.p
+
+ def write_p(self, value):
+ self._dev.p = value
+
+ def read_i(self):
+ return self._dev.i
+
+ def write_i(self, value):
+ self._dev.i = value
+
+ def read_d(self):
+ return self._dev.d
+
+ def write_d(self, value):
+ self._dev.d = value
+
+ def read_pid(self):
+ self.read_p()
+ self.read_i()
+ self.read_d()
+ return self.p, self.i, self.d
+
+ def write_pid(self, value):
+ self._dev.p = value[0]
+ self._dev.i = value[1]
+ self._dev.d = value[2]
+
+ def read_setpoint(self):
+ return self._dev.setpoint
+
+ def read_heateroutput(self):
+ return self._dev.heaterOutput
+
+
+class PowerSupply(Actuator):
+ """
+ A power supply (voltage and current) device.
+ """
+
+ PARAMS = {
+ 'ramp': PARAM('Current/voltage ramp', unit='main/min',
+ datatype=FloatRange(), readonly=False, poll=30,),
+ 'voltage': PARAM('Actual voltage', unit='V',
+ datatype=FloatRange(), poll=5),
+ 'current': PARAM('Actual current', unit='A',
+ datatype=FloatRange(), poll=5),
+ }
+
+ def read_ramp(self):
+ return self._dev.ramp
+
+ def write_ramp(self, value):
+ self._dev.ramp = value
+
+ def read_voltage(self):
+ return self._dev.voltage
+
+ def read_current(self):
+ return self._dev.current
+
+
+class DigitalInput(PyTangoDevice, Readable):
+ """
+ A device reading a bitfield.
+ """
+
+ OVERRIDES = {
+ 'value': OVERRIDE(datatype=IntRange(0)),
+ }
+
+ def read_value(self, maxage=0):
+ return self._dev.value
+
+
+class NamedDigitalInput(DigitalInput):
+ """
+ A DigitalInput with numeric values mapped to names.
+ """
+
+ PARAMS = {
+ 'mapping': PARAM('A dictionary mapping state names to integers',
+ datatype=StringType(), export=False), # XXX:!!!
+ }
+
+ def init(self):
+ super(NamedDigitalInput, self).init()
+ try:
+ self.PARAMS['value'].datatype = EnumType(**eval(self.mapping))
+ except Exception as e:
+ raise ValueError('Illegal Value for mapping: %r' % e)
+
+ def read_value(self, maxage=0):
+ value = self._dev.value
+ return value # mapping is done by datatype upon export()
+
+
+class PartialDigitalInput(NamedDigitalInput):
+ """
+ Base class for a TANGO DigitalInput with only a part of the full
+ bit width accessed.
+ """
+
+ PARAMS = {
+ 'startbit': PARAM(
+ 'Number of the first bit',
+ datatype=IntRange(0),
+ default=0),
+ 'bitwidth': PARAM(
+ 'Number of bits',
+ datatype=IntRange(0),
+ default=1),
+ }
+
+ def init(self):
+ super(PartialDigitalInput, self).init()
+ self._mask = (1 << self.bitwidth) - 1
+ #self.PARAMS['value'].datatype = IntRange(0, self._mask)
+
+ def read_value(self, maxage=0):
+ raw_value = self._dev.value
+ value = (raw_value >> self.startbit) & self._mask
+ return value # mapping is done by datatype upon export()
+
+
+class DigitalOutput(PyTangoDevice, Driveable):
+ """
+ A devices that can set and read a digital value corresponding to a
+ bitfield.
+ """
+
+ OVERRIDES = {
+ 'value': OVERRIDE(datatype=IntRange(0)),
+ 'target': OVERRIDE(datatype=IntRange(0)),
+ }
+
+ def read_value(self, maxage=0):
+ return self._dev.value # mapping is done by datatype upon export()
+
+ def write_target(self, value):
+ self._dev.value = value
+ self.read_value()
+
+ def read_target(self, maxage=0):
+ attrObj = self._dev.read_attribute('value')
+ return attrObj.w_value
+
+
+class NamedDigitalOutput(DigitalOutput):
+ """
+ A DigitalOutput with numeric values mapped to names.
+ """
+
+ PARAMS = {
+ 'mapping': PARAM('A dictionary mapping state names to integers',
+ datatype=StringType(), export=False), # XXX: !!!
+ }
+
+ def init(self):
+ super(NamedDigitalOutput, self).init()
+ try: # XXX: !!!
+ self.PARAMS['value'].datatype = EnumType(**eval(self.mapping))
+ except Exception as e:
+ raise ValueError('Illegal Value for mapping: %r' % e)
+
+
+class PartialDigitalOutput(NamedDigitalOutput):
+ """
+ Base class for a TANGO DigitalOutput with only a part of the full
+ bit width accessed.
+ """
+
+ PARAMS = {
+ 'startbit': PARAM(
+ 'Number of the first bit',
+ datatype=IntRange(0),
+ default=0),
+ 'bitwidth': PARAM(
+ 'Number of bits',
+ datatype=IntRange(0),
+ default=1),
+ }
+
+ def init(self, mode):
+ super(PartialDigitalOutput, self).init()
+ self._mask = (1 << self.bitwidth) - 1
+ #self.PARAMS['value'].datatype = IntRange(0, self._mask)
+ #self.PARAMS['target'].datatype = IntRange(0, self._mask)
+
+ def read_value(self, maxage=0):
+ raw_value = self._dev.value
+ value = (raw_value >> self.startbit) & self._mask
+ return value # mapping is done by datatype upon export()
+
+ def write_target(self, target):
+ curvalue = self._dev.value
+ newvalue = (curvalue & ~(self._mask << self.startbit)) | \
+ (target << self.startbit)
+ self._dev.value = newvalue
+ self.read_value()
+
+
+class StringIO(PyTangoDevice, Device):
+ """
+ StringIO abstracts communication over a hardware bus that sends and
+ receives strings.
+ """
+
+ PARAMS = {
+ 'bustimeout': PARAM(
+ 'Communication timeout',
+ datatype=FloatRange(),
+ readonly=False,
+ unit='s',
+ group='communication'),
+ 'endofline': PARAM(
+ 'End of line',
+ datatype=StringType(),
+ readonly=False,
+ group='communication'),
+ 'startofline': PARAM(
+ 'Start of line',
+ datatype=StringType(),
+ readonly=False,
+ group='communication'),
+ }
+
+ def read_bustimeout(self):
+ return self._dev.communicationTimeout
+
+ def write_bustimeout(self, value):
+ self._dev.communicationTimeout = value
+
+ def read_endofline(self):
+ return self._dev.endOfLine
+
+ def write_endofline(self, value):
+ self._dev.endOfLine = value
+
+ def read_startofline(self):
+ return self._dev.startOfLine
+
+ def write_startofline(self, value):
+ self._dev.startOfLine = value
+
+ CMDS = {
+ 'communicate': CMD(
+ 'Send a string and return the reply',
+ arguments=[
+ StringType()],
+ result=StringType()),
+ 'flush': CMD(
+ 'Flush output buffer',
+ arguments=[],
+ result=None),
+ 'read': CMD(
+ 'read some characters from input buffer',
+ arguments=[
+ IntRange()],
+ result=StringType()),
+ 'write': CMD(
+ 'write some chars to output',
+ arguments=[
+ StringType()],
+ result=None),
+ 'readLine': CMD(
+ 'Read sol - a whole line - eol',
+ arguments=[],
+ result=StringType()),
+ 'writeLine': CMD(
+ 'write sol + a whole line + eol',
+ arguments=[
+ StringType()],
+ result=None),
+ 'availablechars': CMD(
+ 'return number of chars in input buffer',
+ arguments=[],
+ result=IntRange(0)),
+ 'availablelines': CMD(
+ 'return number of lines in input buffer',
+ arguments=[],
+ result=IntRange(0)),
+ 'multicommunicate': CMD(
+ 'perform a sequence of communications',
+ arguments=[
+ ArrayOf(
+ TupleOf(
+ StringType(),
+ IntRange()))],
+ result=ArrayOf(
+ StringType())),
+ }
+
+ def do_communicate(self, value=StringType()):
+ return self._dev.Communicate(value)
+
+ def do_flush(self):
+ self._dev.Flush()
+
+ def do_read(self, value):
+ return self._dev.Read(value)
+
+ def do_write(self, value):
+ return self._dev.Write(value)
+
+ def do_readLine(self):
+ return self._dev.ReadLine()
+
+ def do_writeLine(self, value):
+ return self._dev.WriteLine(value)
+
+ def do_multiCommunicate(self, value):
+ return self._dev.MultiCommunicate(value)
+
+ def do_availablechars(self):
+ return self._dev.availableChars
+
+ def do_availablelines(self):
+ return self._dev.availableLines
diff --git a/test/test_client_baseclient.py b/test/test_client_baseclient.py
new file mode 100644
index 0000000..ea746d6
--- /dev/null
+++ b/test/test_client_baseclient.py
@@ -0,0 +1,83 @@
+# -*- 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:
+# Enrico Faulhaber
+#
+# *****************************************************************************
+"""test base client."""
+
+import pytest
+
+import sys
+sys.path.insert(0, sys.path[0]+'/..')
+
+from collections import OrderedDict
+from secop.client.baseclient import Client
+
+# define Test-only connection object
+class TestConnect(object):
+ callbacks = []
+ def writeline(self, line):
+ pass
+
+ def readline(self):
+ return ''
+
+
+@pytest.fixture(scope="module")
+def clientobj(request):
+ print (" SETUP ClientObj")
+ testconnect = TestConnect()
+ yield Client(dict(testing=testconnect), autoconnect=False)
+ for cb, arg in testconnect.callbacks:
+ cb(arg)
+ print (" TEARDOWN ClientObj")
+
+
+def test_describing_data_decode(clientobj):
+ assert OrderedDict([('a',1)]) == clientobj._decode_list_to_ordereddict(['a',1])
+ assert {'modules':{}, 'properties':{}} == clientobj._decode_substruct(['modules'],{})
+ describing_data = {'equipment_id': 'eid',
+ 'modules': ['LN2', {'commands': [],
+ 'interfaces': ['Readable', 'Device'],
+ 'parameters': ['value', {'datatype': ['double'],
+ 'description': 'current value',
+ 'readonly': True,
+ }
+ ]
+ }
+ ]
+ }
+ decoded_data = {'modules': {'LN2': {'commands': {},
+ 'parameters': {'value': {'datatype': ['double'],
+ 'description': 'current value',
+ 'readonly': True,
+ }
+ },
+ 'properties': {'interfaces': ['Readable', 'Device']}
+ }
+ },
+ 'properties': {'equipment_id': 'eid',
+ }
+ }
+
+ a = clientobj._decode_substruct(['modules'], describing_data)
+ for modname, module in a['modules'].items():
+ a['modules'][modname] = clientobj._decode_substruct(['parameters', 'commands'], module)
+ assert a == decoded_data
+