several improvements and bugfixes

+ rework GUI
  - include a combobox for selection of visibility
  - include a checkbox wether validation should be done in the client
  - remove unused lineEdit
+ improve datatypes
+ improve tests for new descriptive data
+ metaclasse: fix overlooked read_* or write_* func's
+ improve polling
+ Introduce new ErrorClasses
+ dispatcher: use new features of datatypes + PARAMS
+ improve lib
+ autopep8
+ first working version of MLZ_entangle integration
+ split specific stuff into it's own package (MLZ,demo,ess)

Change-Id: I8ac3ce871b28f44afecbba6332ca741095426712
This commit is contained in:
Enrico Faulhaber 2017-07-09 22:18:01 +02:00 committed by Alexander Lenz
parent 8a63a6c63f
commit 29ee07c5b3
25 changed files with 1830 additions and 220 deletions

45
etc/ccr12.cfg Normal file
View File

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

View File

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

View File

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

View File

@ -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"

View File

@ -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"

View File

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

View File

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

View File

@ -33,3 +33,11 @@ class ConfigError(SECoPServerError):
class ProgrammingError(SECoPServerError):
pass
class CommunicationError(SECoPServerError):
pass
class HardwareError(SECoPServerError):
pass

View File

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

View File

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

View File

@ -14,8 +14,8 @@
<string>secop-gui</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QSplitter" name="splitter">
<property name="orientation">
<enum>Qt::Horizontal</enum>
@ -23,7 +23,53 @@
<widget class="QWidget" name="layoutWidget">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLineEdit" name="lineEdit"/>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="topMargin">
<number>0</number>
</property>
<item>
<widget class="QComboBox" name="visibilityComboBox">
<item>
<property name="text">
<string>user</string>
</property>
</item>
<item>
<property name="text">
<string>admin</string>
</property>
</item>
<item>
<property name="text">
<string>expert</string>
</property>
</item>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QCheckBox" name="validateCheckBox">
<property name="text">
<string>Validate locally</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QTreeWidget" name="treeWidget">
@ -58,7 +104,7 @@
<x>0</x>
<y>0</y>
<width>1228</width>
<height>25</height>
<height>23</height>
</rect>
</property>
<widget class="QMenu" name="menuFile">

View File

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>230</width>
<height>121</height>
<height>195</height>
</rect>
</property>
<property name="windowTitle">
@ -92,6 +92,40 @@
</property>
</spacer>
</item>
<item row="4" column="0">
<widget class="QGroupBox" name="propertyGroupBox">
<property name="title">
<string>Properties:</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
</layout>
</widget>
</item>
<item row="5" column="0">
<spacer name="verticalSpacer_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>

View File

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>730</width>
<height>33</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<property name="horizontalSpacing">
<number>6</number>
</property>
<property name="verticalSpacing">
<number>0</number>
</property>
<property name="margin">
<number>0</number>
</property>
<item row="0" column="2">
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Current: </string>
</property>
</widget>
</item>
<item row="0" column="3">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Set: </string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="targetValueComboBox"/>
</item>
<item row="0" column="4">
<widget class="QComboBox" name="comboBox_2"/>
</item>
<item row="0" column="5">
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

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

View File

@ -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?

View File

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

View File

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

View File

@ -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()), ), }

View File

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

0
secop_ess/__init__.py Normal file
View File

View File

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

0
secop_mlz/__init__.py Normal file
View File

1024
secop_mlz/entangle.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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 <enrico.faulhaber@frm2.tum.de>
#
# *****************************************************************************
"""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