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:
parent
8a63a6c63f
commit
29ee07c5b3
45
etc/ccr12.cfg
Normal file
45
etc/ccr12.cfg
Normal 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)
|
||||
|
@ -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
|
||||
|
14
etc/demo.cfg
14
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
|
||||
|
@ -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"
|
||||
|
||||
|
10
etc/test.cfg
10
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"
|
||||
|
||||
|
@ -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
|
||||
|
@ -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]
|
||||
@ -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,11 +317,17 @@ 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
|
||||
@ -275,9 +337,11 @@ class ArrayOf(DataType):
|
||||
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:
|
||||
|
@ -33,3 +33,11 @@ class ConfigError(SECoPServerError):
|
||||
|
||||
class ProgrammingError(SECoPServerError):
|
||||
pass
|
||||
|
||||
|
||||
class CommunicationError(SECoPServerError):
|
||||
pass
|
||||
|
||||
|
||||
class HardwareError(SECoPServerError):
|
||||
pass
|
||||
|
@ -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))
|
||||
|
@ -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:
|
||||
|
@ -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">
|
||||
|
@ -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/>
|
||||
|
76
secop/gui/ui/parambuttons_select.ui
Normal file
76
secop/gui/ui/parambuttons_select.ui
Normal 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>
|
@ -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
|
||||
if modname.startswith('secop'):
|
||||
module = importlib.import_module(modname)
|
||||
else:
|
||||
# rarely needed by now....
|
||||
module = importlib.import_module('secop.' + modname)
|
||||
# module = __import__(spec)
|
||||
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)
|
||||
|
@ -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__
|
||||
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__
|
||||
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,23 +414,36 @@ 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:
|
||||
i = 1
|
||||
try:
|
||||
time.sleep(self.pollinterval)
|
||||
for pname in self.PARAMS:
|
||||
if pname != 'pollinterval':
|
||||
rfunc = getattr(self, 'read_%s' % pname, None)
|
||||
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()
|
||||
|
||||
@ -387,7 +454,10 @@ 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,
|
||||
'target': PARAM(
|
||||
'target value of the device',
|
||||
default=0.,
|
||||
readonly=False,
|
||||
datatype=FloatRange(),
|
||||
),
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
@ -125,8 +125,10 @@ class Cryostat(CryoBase):
|
||||
),
|
||||
)
|
||||
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):
|
@ -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()), ), }
|
@ -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,11 +63,19 @@ class Temp(Driveable):
|
||||
but the implementation may do anything
|
||||
"""
|
||||
PARAMS = {
|
||||
'sensor': PARAM("Sensor number or calibration id",
|
||||
datatype=StringType(8,16), readonly=True,
|
||||
'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',
|
||||
'target': PARAM(
|
||||
"Target temperature",
|
||||
default=300.0,
|
||||
datatype=FloatRange(0),
|
||||
readonly=False,
|
||||
unit='K',
|
||||
),
|
||||
}
|
||||
|
0
secop_ess/__init__.py
Normal file
0
secop_ess/__init__.py
Normal 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:
|
||||
@ -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"""
|
||||
@ -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
0
secop_mlz/__init__.py
Normal file
1024
secop_mlz/entangle.py
Normal file
1024
secop_mlz/entangle.py
Normal file
File diff suppressed because it is too large
Load Diff
83
test/test_client_baseclient.py
Normal file
83
test/test_client_baseclient.py
Normal 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
|
||||
|
Loading…
x
Reference in New Issue
Block a user