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:

committed by
Alexander Lenz

parent
8a63a6c63f
commit
29ee07c5b3
@ -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]
|
||||
@ -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:
|
||||
|
@ -1,374 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- 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>
|
||||
# *****************************************************************************
|
||||
"""playing implementation of a (simple) simulated cryostat"""
|
||||
|
||||
from math import atan
|
||||
import time
|
||||
import random
|
||||
import threading
|
||||
|
||||
from secop.devices.core import Driveable, CMD, PARAM
|
||||
from secop.protocol import status
|
||||
from secop.datatypes import FloatRange, EnumType, TupleOf
|
||||
from secop.lib import clamp, mkthread
|
||||
|
||||
|
||||
class CryoBase(Driveable):
|
||||
pass
|
||||
|
||||
|
||||
class Cryostat(CryoBase):
|
||||
"""simulated cryostat with:
|
||||
|
||||
- heat capacity of the sample
|
||||
- cooling power
|
||||
- thermal transfer between regulation and samplen
|
||||
"""
|
||||
PARAMS = dict(
|
||||
jitter=PARAM("amount of random noise on readout values",
|
||||
datatype=FloatRange(0, 1), unit="K",
|
||||
default=0.1, readonly=False, export=False,
|
||||
),
|
||||
T_start=PARAM("starting temperature for simulation",
|
||||
datatype=FloatRange(0), default=10,
|
||||
export=False,
|
||||
),
|
||||
looptime=PARAM("timestep for simulation",
|
||||
datatype=FloatRange(0.01, 10), unit="s", default=1,
|
||||
readonly=False, export=False,
|
||||
),
|
||||
ramp=PARAM("ramping speed of the setpoint",
|
||||
datatype=FloatRange(0, 1e3), unit="K/min", default=1,
|
||||
readonly=False,
|
||||
),
|
||||
setpoint=PARAM("current setpoint during ramping else target",
|
||||
datatype=FloatRange(), default=1, unit='K',
|
||||
),
|
||||
maxpower=PARAM("Maximum heater power",
|
||||
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,
|
||||
),
|
||||
value=PARAM("regulation temperature",
|
||||
datatype=FloatRange(0), default=0, unit="K",
|
||||
),
|
||||
pid=PARAM("regulation coefficients",
|
||||
datatype=TupleOf(FloatRange(0), FloatRange(0, 100),
|
||||
FloatRange(0, 100)),
|
||||
default=(40, 10, 2), readonly=False,
|
||||
group='pid',
|
||||
),
|
||||
p=PARAM("regulation coefficient 'p'",
|
||||
datatype=FloatRange(0), default=40, unit="%/K", readonly=False,
|
||||
group='pid',
|
||||
),
|
||||
i=PARAM("regulation coefficient 'i'",
|
||||
datatype=FloatRange(0, 100), default=10, readonly=False,
|
||||
group='pid',
|
||||
),
|
||||
d=PARAM("regulation coefficient 'd'",
|
||||
datatype=FloatRange(0, 100), default=2, readonly=False,
|
||||
group='pid',
|
||||
),
|
||||
mode=PARAM("mode of regulation",
|
||||
datatype=EnumType('ramp', 'pid', 'openloop'),
|
||||
default='ramp',
|
||||
readonly=False,
|
||||
),
|
||||
pollinterval=PARAM("polling interval",
|
||||
datatype=FloatRange(0), default=5,
|
||||
),
|
||||
tolerance=PARAM("temperature range for stability checking",
|
||||
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),
|
||||
)
|
||||
|
||||
def init(self):
|
||||
self._stopflag = False
|
||||
self._thread = mkthread(self.thread)
|
||||
|
||||
def read_status(self, maxage=0):
|
||||
# instead of asking a 'Hardware' take the value from the simulation
|
||||
return self.status
|
||||
|
||||
def read_value(self, maxage=0):
|
||||
# return regulation value (averaged regulation temp)
|
||||
return self.regulationtemp + \
|
||||
self.jitter * (0.5 - random.random())
|
||||
|
||||
def read_target(self, maxage=0):
|
||||
return self.target
|
||||
|
||||
def write_target(self, value):
|
||||
value = round(value, 2)
|
||||
if value == self.target:
|
||||
# nothing to do
|
||||
return value
|
||||
self.target = value
|
||||
# next read_status will see this status, until the loop updates it
|
||||
self.status = status.BUSY, 'new target set'
|
||||
return value
|
||||
|
||||
def read_maxpower(self, maxage=0):
|
||||
return self.maxpower
|
||||
|
||||
def write_maxpower(self, newpower):
|
||||
# rescale heater setting in % to keep the power
|
||||
heat = max(0, min(100, self.heater * self.maxpower / float(newpower)))
|
||||
self.heater = heat
|
||||
self.maxpower = newpower
|
||||
return newpower
|
||||
|
||||
def write_pid(self, newpid):
|
||||
self.p, self.i, self.d = newpid
|
||||
return (self.p, self.i, self.d)
|
||||
|
||||
def read_pid(self, maxage=0):
|
||||
return (self.p, self.i, self.d)
|
||||
|
||||
def doStop(self):
|
||||
# stop the ramp by setting current setpoint as target
|
||||
# XXX: discussion: take setpoint or current value ???
|
||||
self.write_target(self.setpoint)
|
||||
|
||||
#
|
||||
# calculation helpers
|
||||
#
|
||||
def __coolerPower(self, temp):
|
||||
"""returns cooling power in W at given temperature"""
|
||||
# quadratic up to 42K, is linear from 40W@42K to 100W@600K
|
||||
# return clamp((temp-2)**2 / 32., 0., 40.) + temp * 0.1
|
||||
return clamp(15 * atan(temp * 0.01)**3, 0., 40.) + temp * 0.1 - 0.2
|
||||
|
||||
def __coolerCP(self, temp):
|
||||
"""heat capacity of cooler at given temp"""
|
||||
return 75 * atan(temp / 50)**2 + 1
|
||||
|
||||
def __heatLink(self, coolertemp, sampletemp):
|
||||
"""heatflow from sample to cooler. may be negative..."""
|
||||
flow = (sampletemp - coolertemp) * \
|
||||
((coolertemp + sampletemp) ** 2) / 400.
|
||||
cp = clamp(
|
||||
self.__coolerCP(coolertemp) * self.__sampleCP(sampletemp), 1, 10)
|
||||
return clamp(flow, -cp, cp)
|
||||
|
||||
def __sampleCP(self, temp):
|
||||
return 3 * atan(temp / 30) + \
|
||||
12 * temp / ((temp - 12.)**2 + 10) + 0.5
|
||||
|
||||
def __sampleLeak(self, temp):
|
||||
return 0.02 / temp
|
||||
|
||||
def thread(self):
|
||||
self.sampletemp = self.T_start
|
||||
self.regulationtemp = self.T_start
|
||||
self.status = status.OK, ''
|
||||
while not self._stopflag:
|
||||
try:
|
||||
self.__sim()
|
||||
except Exception as e:
|
||||
self.log.exception(e)
|
||||
self.status = status.ERROR, str(e)
|
||||
|
||||
def __sim(self):
|
||||
# complex thread handling:
|
||||
# a) simulation of cryo (heat flow, thermal masses,....)
|
||||
# b) optional PID temperature controller with windup control
|
||||
# c) generating status+updated value+ramp
|
||||
# this thread is not supposed to exit!
|
||||
|
||||
self.setpoint = self.target
|
||||
# local state keeping:
|
||||
regulation = self.regulationtemp
|
||||
sample = self.sampletemp
|
||||
# keep history values for stability check
|
||||
window = []
|
||||
timestamp = time.time()
|
||||
heater = 0
|
||||
lastflow = 0
|
||||
last_heaters = (0, 0)
|
||||
delta = 0
|
||||
I = D = 0
|
||||
lastD = 0
|
||||
damper = 1
|
||||
lastmode = self.mode
|
||||
while not self._stopflag:
|
||||
t = time.time()
|
||||
h = t - timestamp
|
||||
if h < self.looptime / damper:
|
||||
time.sleep(clamp(self.looptime / damper - h, 0.1, 60))
|
||||
continue
|
||||
# a)
|
||||
sample = self.sampletemp
|
||||
regulation = self.regulationtemp
|
||||
heater = self.heater
|
||||
|
||||
heatflow = self.__heatLink(regulation, sample)
|
||||
self.log.debug('sample = %.5f, regulation = %.5f, heatflow = %.5g'
|
||||
% (sample, regulation, heatflow))
|
||||
newsample = max(0, sample + (self.__sampleLeak(sample) - heatflow)
|
||||
/ self.__sampleCP(sample) * h)
|
||||
# avoid instabilities due to too small CP
|
||||
newsample = clamp(newsample, sample, regulation)
|
||||
regdelta = (heater * 0.01 * self.maxpower + heatflow -
|
||||
self.__coolerPower(regulation))
|
||||
newregulation = max(
|
||||
0, regulation + regdelta / self.__coolerCP(regulation) * h)
|
||||
# b) see
|
||||
# http://brettbeauregard.com/blog/2011/04/
|
||||
# improving-the-beginners-pid-introduction/
|
||||
if self.mode != 'openloop':
|
||||
# fix artefacts due to too big timesteps
|
||||
# actually i would prefer reducing looptime, but i have no
|
||||
# good idea on when to increase it back again
|
||||
if heatflow * lastflow != -100:
|
||||
if (newregulation - newsample) * (regulation - sample) < 0:
|
||||
# newregulation = (newregulation + regulation) / 2
|
||||
# newsample = (newsample + sample) / 2
|
||||
damper += 1
|
||||
lastflow = heatflow
|
||||
|
||||
error = self.setpoint - newregulation
|
||||
# use a simple filter to smooth delta a little
|
||||
delta = (delta + regulation - newregulation) / 2.
|
||||
|
||||
kp = self.p / 10. # LakeShore P = 10*k_p
|
||||
ki = kp * abs(self.i) / 500. # LakeShore I = 500/T_i
|
||||
kd = kp * abs(self.d) / 2. # LakeShore D = 2*T_d
|
||||
P = kp * error
|
||||
I += ki * error * h
|
||||
D = kd * delta / h
|
||||
|
||||
# avoid reset windup
|
||||
I = clamp(I, 0., 100.) # I is in %
|
||||
|
||||
# avoid jumping heaterpower if switching back to pid mode
|
||||
if lastmode != self.mode:
|
||||
# adjust some values upon switching back on
|
||||
I = self.heater - P - D
|
||||
|
||||
v = P + I + D
|
||||
# in damping mode, use a weighted sum of old + new heaterpower
|
||||
if damper > 1:
|
||||
v = ((damper**2 - 1) * self.heater + v) / damper**2
|
||||
|
||||
# damp oscillations due to D switching signs
|
||||
if D * lastD < -0.2:
|
||||
v = (v + heater) / 2.
|
||||
# clamp new heater power to 0..100%
|
||||
heater = clamp(v, 0., 100.)
|
||||
lastD = D
|
||||
|
||||
self.log.debug('PID: P = %.2f, I = %.2f, D = %.2f, '
|
||||
'heater = %.2f' % (P, I, D, heater))
|
||||
|
||||
# check for turn-around points to detect oscillations ->
|
||||
# increase damper
|
||||
x, y = last_heaters
|
||||
if (x + 0.1 < y and y > heater + 0.1) or \
|
||||
(x > y + 0.1 and y + 0.1 < heater):
|
||||
damper += 1
|
||||
last_heaters = (y, heater)
|
||||
|
||||
else:
|
||||
# self.heaterpower is set manually, not by pid
|
||||
heater = self.heater
|
||||
last_heaters = (0, 0)
|
||||
|
||||
heater = round(heater, 1)
|
||||
sample = newsample
|
||||
regulation = newregulation
|
||||
lastmode = self.mode
|
||||
# c)
|
||||
if self.setpoint != self.target:
|
||||
if self.ramp == 0:
|
||||
maxdelta = 10000
|
||||
else:
|
||||
maxdelta = self.ramp / 60. * h
|
||||
try:
|
||||
self.setpoint = round(self.setpoint + clamp(
|
||||
self.target - self.setpoint, -maxdelta, maxdelta), 3)
|
||||
self.log.debug('setpoint changes to %r (target %r)' %
|
||||
(self.setpoint, self.target))
|
||||
except (TypeError, ValueError):
|
||||
# self.target might be None
|
||||
pass
|
||||
|
||||
# temperature is stable when all recorded values in the window
|
||||
# differ from setpoint by less than tolerance
|
||||
currenttime = time.time()
|
||||
window.append((currenttime, sample))
|
||||
while window[0][0] < currenttime - self.window:
|
||||
# remove old/stale entries
|
||||
window.pop(0)
|
||||
# obtain min/max
|
||||
deviation = 0
|
||||
for _, T in window:
|
||||
if abs(T - self.target) > deviation:
|
||||
deviation = abs(T - self.target)
|
||||
if (len(window) < 3) or deviation > self.tolerance:
|
||||
self.status = status.BUSY, 'unstable'
|
||||
elif self.setpoint == self.target:
|
||||
self.status = status.OK, 'at target'
|
||||
damper -= (damper - 1) / 10. # max value for damper is 11
|
||||
else:
|
||||
self.status = status.BUSY, 'ramping setpoint'
|
||||
damper -= (damper - 1) / 20.
|
||||
self.regulationtemp = round(regulation, 3)
|
||||
self.sampletemp = round(sample, 3)
|
||||
self.heaterpower = round(heater * self.maxpower * 0.01, 3)
|
||||
self.heater = heater
|
||||
timestamp = t
|
||||
self.read_value()
|
||||
|
||||
def shutdown(self):
|
||||
# should be called from server when the server is stopped
|
||||
self._stopflag = True
|
||||
if self._thread and self._thread.isAlive():
|
||||
self._thread.join()
|
@ -1,304 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- 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>
|
||||
# *****************************************************************************
|
||||
"""testing devices"""
|
||||
|
||||
import time
|
||||
import random
|
||||
import threading
|
||||
|
||||
from secop.devices.core import Readable, Driveable, PARAM
|
||||
from secop.datatypes import EnumType, FloatRange, IntRange, ArrayOf, StringType, TupleOf, StructOf, BoolType
|
||||
from secop.protocol import status
|
||||
|
||||
|
||||
class Switch(Driveable):
|
||||
"""switch it on or off....
|
||||
"""
|
||||
PARAMS = {
|
||||
'value': PARAM('current state (on or off)',
|
||||
datatype=EnumType(on=1, off=0), default=0,
|
||||
),
|
||||
'target': PARAM('wanted state (on or off)',
|
||||
datatype=EnumType(on=1, off=0), default=0,
|
||||
readonly=False,
|
||||
),
|
||||
'switch_on_time': PARAM('seconds to wait after activating the switch',
|
||||
datatype=FloatRange(0, 60), unit='s',
|
||||
default=10, export=False,
|
||||
),
|
||||
'switch_off_time': PARAM('cool-down time in seconds',
|
||||
datatype=FloatRange(0, 60), unit='s',
|
||||
default=10, export=False,
|
||||
),
|
||||
}
|
||||
|
||||
def init(self):
|
||||
self._started = 0
|
||||
|
||||
def read_value(self, maxage=0):
|
||||
# could ask HW
|
||||
# we just return the value of the target here.
|
||||
self._update()
|
||||
return self.value
|
||||
|
||||
def read_target(self, maxage=0):
|
||||
# could ask HW
|
||||
return self.target
|
||||
|
||||
def write_target(self, value):
|
||||
# could tell HW
|
||||
pass
|
||||
# note: setting self.target to the new value is done after this....
|
||||
# note: we may also return the read-back value from the hw here
|
||||
|
||||
def read_status(self, maxage=0):
|
||||
self.log.info("read status")
|
||||
info = self._update()
|
||||
if self.target == self.value:
|
||||
return status.OK, ''
|
||||
return status.BUSY, info
|
||||
|
||||
def _update(self):
|
||||
started = self.PARAMS['target'].timestamp
|
||||
info = ''
|
||||
if self.target > self.value:
|
||||
info = 'waiting for ON'
|
||||
if time.time() > started + self.switch_on_time:
|
||||
info = 'is switched ON'
|
||||
self.value = self.target
|
||||
elif self.target < self.value:
|
||||
info = 'waiting for OFF'
|
||||
if time.time() > started + self.switch_off_time:
|
||||
info = 'is switched OFF'
|
||||
self.value = self.target
|
||||
if info:
|
||||
self.log.debug(info)
|
||||
return info
|
||||
|
||||
|
||||
class MagneticField(Driveable):
|
||||
"""a liquid magnet
|
||||
"""
|
||||
PARAMS = {
|
||||
'value': PARAM('current field in T',
|
||||
unit='T', datatype=FloatRange(-15, 15), default=0,
|
||||
),
|
||||
'target': PARAM('target field in T',
|
||||
unit='T', datatype=FloatRange(-15, 15), default=0,
|
||||
readonly=False,
|
||||
),
|
||||
'ramp': PARAM('ramping speed',
|
||||
unit='T/min', datatype=FloatRange(0, 1), default=0.1,
|
||||
readonly=False,
|
||||
),
|
||||
'mode': PARAM('what to do after changing field',
|
||||
default=1, datatype=EnumType(persistent=1, hold=0),
|
||||
readonly=False,
|
||||
),
|
||||
'heatswitch': PARAM('name of heat switch device',
|
||||
datatype=StringType(), export=False,
|
||||
),
|
||||
}
|
||||
|
||||
def init(self):
|
||||
self._state = 'idle'
|
||||
self._heatswitch = self.DISPATCHER.get_module(self.heatswitch)
|
||||
_thread = threading.Thread(target=self._thread)
|
||||
_thread.daemon = True
|
||||
_thread.start()
|
||||
|
||||
def read_value(self, maxage=0):
|
||||
return self.value
|
||||
|
||||
def write_target(self, value):
|
||||
# could tell HW
|
||||
return round(value, 2)
|
||||
# note: setting self.target to the new value is done after this....
|
||||
# note: we may also return the read-back value from the hw here
|
||||
|
||||
def read_status(self, maxage=0):
|
||||
return (status.OK, '') if self._state == 'idle' else (status.BUSY,
|
||||
self._state)
|
||||
|
||||
def _thread(self):
|
||||
loopdelay = 1
|
||||
while True:
|
||||
ts = time.time()
|
||||
if self._state == 'idle':
|
||||
if self.target != self.value:
|
||||
self.log.debug('got new target -> switching heater on')
|
||||
self._state = 'switch_on'
|
||||
self._heatswitch.write_target('on')
|
||||
if self._state == 'switch_on':
|
||||
# wait until switch is on
|
||||
if self._heatswitch.read_value() == 'on':
|
||||
self.log.debug('heatswitch is on -> ramp to %.3f' %
|
||||
self.target)
|
||||
self._state = 'ramp'
|
||||
if self._state == 'ramp':
|
||||
if self.target == self.value:
|
||||
self.log.debug('at field! mode is %r' % self.mode)
|
||||
if self.mode:
|
||||
self.log.debug('at field -> switching heater off')
|
||||
self._state = 'switch_off'
|
||||
self._heatswitch.write_target('off')
|
||||
else:
|
||||
self.log.debug('at field -> hold')
|
||||
self._state = 'idle'
|
||||
self.status = self.read_status() # push async
|
||||
else:
|
||||
step = self.ramp * loopdelay / 60.
|
||||
step = max(min(self.target - self.value, step), -step)
|
||||
self.value += step
|
||||
if self._state == 'switch_off':
|
||||
# wait until switch is off
|
||||
if self._heatswitch.read_value() == 'off':
|
||||
self.log.debug('heatswitch is off at %.3f' % self.value)
|
||||
self._state = 'idle'
|
||||
self.read_status() # update async
|
||||
time.sleep(max(0.01, ts + loopdelay - time.time()))
|
||||
self.log.error(self, 'main thread exited unexpectedly!')
|
||||
|
||||
|
||||
class CoilTemp(Readable):
|
||||
"""a coil temperature
|
||||
"""
|
||||
PARAMS = {
|
||||
'value': PARAM('Coil temperatur',
|
||||
unit='K', datatype=FloatRange(), default=0,
|
||||
),
|
||||
'sensor': PARAM("Sensor number or calibration id",
|
||||
datatype=StringType(), readonly=True,
|
||||
),
|
||||
}
|
||||
|
||||
def read_value(self, maxage=0):
|
||||
return round(2.3 + random.random(), 3)
|
||||
|
||||
|
||||
class SampleTemp(Driveable):
|
||||
"""a sample temperature
|
||||
"""
|
||||
PARAMS = {
|
||||
'value': PARAM('Sample temperature',
|
||||
unit='K', datatype=FloatRange(), default=10,
|
||||
),
|
||||
'sensor': PARAM("Sensor number or calibration id",
|
||||
datatype=StringType(), readonly=True,
|
||||
),
|
||||
'ramp': PARAM('moving speed in K/min',
|
||||
datatype=FloatRange(0, 100), unit='K/min', default=0.1,
|
||||
readonly=False,
|
||||
),
|
||||
}
|
||||
|
||||
def init(self):
|
||||
_thread = threading.Thread(target=self._thread)
|
||||
_thread.daemon = True
|
||||
_thread.start()
|
||||
|
||||
def write_target(self, value):
|
||||
# could tell HW
|
||||
return round(value, 2)
|
||||
# note: setting self.target to the new value is done after this....
|
||||
# note: we may also return the read-back value from the hw here
|
||||
|
||||
def _thread(self):
|
||||
loopdelay = 1
|
||||
while True:
|
||||
ts = time.time()
|
||||
if self.value == self.target:
|
||||
if self.status != status.OK:
|
||||
self.status = status.OK, ''
|
||||
else:
|
||||
self.status = status.BUSY, 'ramping'
|
||||
step = self.ramp * loopdelay / 60.
|
||||
step = max(min(self.target - self.value, step), -step)
|
||||
self.value += step
|
||||
time.sleep(max(0.01, ts + loopdelay - time.time()))
|
||||
self.log.error(self, 'main thread exited unexpectedly!')
|
||||
|
||||
|
||||
class Label(Readable):
|
||||
"""
|
||||
|
||||
"""
|
||||
PARAMS = {
|
||||
'system': PARAM("Name of the magnet system",
|
||||
datatype=StringType, export=False,
|
||||
),
|
||||
'subdev_mf': PARAM("name of subdevice for magnet status",
|
||||
datatype=StringType, export=False,
|
||||
),
|
||||
'subdev_ts': PARAM("name of subdevice for sample temp",
|
||||
datatype=StringType, export=False,
|
||||
),
|
||||
'value': PARAM("final value of label string",
|
||||
datatype=StringType,
|
||||
),
|
||||
}
|
||||
|
||||
def read_value(self, maxage=0):
|
||||
strings = [self.system]
|
||||
|
||||
dev_ts = self.DISPATCHER.get_module(self.subdev_ts)
|
||||
if dev_ts:
|
||||
strings.append('at %.3f %s' %
|
||||
(dev_ts.read_value(), dev_ts.PARAMS['value'].unit))
|
||||
else:
|
||||
strings.append('No connection to sample temp!')
|
||||
|
||||
dev_mf = self.DISPATCHER.get_module(self.subdev_mf)
|
||||
if dev_mf:
|
||||
mf_stat = dev_mf.read_status()
|
||||
mf_mode = dev_mf.mode
|
||||
mf_val = dev_mf.value
|
||||
mf_unit = dev_mf.PARAMS['value'].unit
|
||||
if mf_stat[0] == status.OK:
|
||||
state = 'Persistent' if mf_mode else 'Non-persistent'
|
||||
else:
|
||||
state = mf_stat[1] or 'ramping'
|
||||
strings.append('%s at %.1f %s' % (state, mf_val, mf_unit))
|
||||
else:
|
||||
strings.append('No connection to magnetic field!')
|
||||
|
||||
return '; '.join(strings)
|
||||
|
||||
|
||||
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()),
|
||||
),
|
||||
}
|
@ -1,232 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- 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>
|
||||
# Erik Dahlbäck <erik.dahlback@esss.se>
|
||||
# *****************************************************************************
|
||||
|
||||
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.protocol import status
|
||||
|
||||
try:
|
||||
from pvaccess import Channel # import EPIVSv4 functionallity, PV access
|
||||
except ImportError:
|
||||
class Channel(object):
|
||||
|
||||
def __init__(self, pv_name):
|
||||
self.pv_name = pv_name
|
||||
self.value = 0.0
|
||||
|
||||
def get(self):
|
||||
return self
|
||||
|
||||
def getDouble(self):
|
||||
return self.value
|
||||
|
||||
def put(self, value):
|
||||
try:
|
||||
self.value = value
|
||||
self.value = float(value)
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
try:
|
||||
from epics import PV
|
||||
except ImportError:
|
||||
class PV(object):
|
||||
|
||||
def __init__(self, pv_name):
|
||||
self.pv_name = pv_name
|
||||
self.value = 0.0
|
||||
|
||||
|
||||
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,),
|
||||
'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),
|
||||
}
|
||||
|
||||
# Generic read and write functions
|
||||
def _read_pv(self, pv_name):
|
||||
if self.epics_version == 'v4':
|
||||
pv_channel = Channel(pv_name)
|
||||
# TODO: cannot handle read of string (is there a .getText() or
|
||||
# .getString() ?)
|
||||
return_value = pv_channel.get().getDouble()
|
||||
else: # Not EPICS v4
|
||||
# TODO: fix this, it does not work
|
||||
pv = PV(pv_name + ".VAL")
|
||||
return_value = pv.value
|
||||
return return_value
|
||||
|
||||
def _write_pv(self, pv_name, write_value):
|
||||
#self.log.info('Write value = %s from EPICS PV = %s' %(write_value, pv_name))
|
||||
# try to convert value to float
|
||||
try:
|
||||
write_value = float(write_value)
|
||||
except (TypeError, ValueError):
|
||||
# can not convert to float, force to string
|
||||
write_value = str(write_value)
|
||||
|
||||
if self.epics_version == 'v4':
|
||||
pv_channel = Channel(pv_name)
|
||||
pv_channel.put(write_value)
|
||||
else: # Not EPICS v4
|
||||
pv = PV(pv_name + ".VAL")
|
||||
pv.value = write_value
|
||||
|
||||
def read_value(self, maxage=0):
|
||||
return self._read_pv(self.value_pv)
|
||||
|
||||
def read_status(self, maxage=0):
|
||||
# XXX: comparison may need to be a little unsharp
|
||||
# XXX: Hardware may have it's own idea about the status: how to obtain?
|
||||
if self.status_pv != 'unset':
|
||||
# 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
|
||||
return (status.OK, 'no pv set')
|
||||
|
||||
|
||||
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,),
|
||||
'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),
|
||||
}
|
||||
|
||||
# Generic read and write functions
|
||||
def _read_pv(self, pv_name):
|
||||
if self.epics_version == 'v4':
|
||||
pv_channel = Channel(pv_name)
|
||||
# TODO: cannot handle read of string (is there a .getText() or
|
||||
# .getString() ?)
|
||||
return_value = pv_channel.get().getDouble()
|
||||
else: # Not EPICS v4
|
||||
# TODO: fix this, it does not work
|
||||
pv = PV(pv_name + ".VAL")
|
||||
return_value = pv.value
|
||||
return return_value
|
||||
|
||||
def _write_pv(self, pv_name, write_value):
|
||||
#self.log.info('Write value = %s from EPICS PV = %s' %(write_value, pv_name))
|
||||
# try to convert value to float
|
||||
try:
|
||||
write_value = float(write_value)
|
||||
except (TypeError, ValueError):
|
||||
# can not convert to float, force to string
|
||||
write_value = str(write_value)
|
||||
|
||||
if self.epics_version == 'v4':
|
||||
pv_channel = Channel(pv_name)
|
||||
pv_channel.put(write_value)
|
||||
else: # Not EPICS v4
|
||||
pv = PV(pv_name + ".VAL")
|
||||
pv.value = write_value
|
||||
|
||||
def read_target(self, maxage=0):
|
||||
return self._read_pv(self.target_pv)
|
||||
|
||||
def write_target(self, write_value):
|
||||
self._write_pv(self.target_pv, write_value)
|
||||
|
||||
def read_value(self, maxage=0):
|
||||
return self._read_pv(self.value_pv)
|
||||
|
||||
def read_status(self, maxage=0):
|
||||
# XXX: comparison may need to be a little unsharp
|
||||
# XXX: Hardware may have it's own idea about the status: how to obtain?
|
||||
if self.status_pv != 'unset':
|
||||
# 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')
|
||||
|
||||
|
||||
"""Temperature control loop"""
|
||||
# should also derive from secop.core.temperaturecontroller, once its
|
||||
# features are agreed upon
|
||||
|
||||
|
||||
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,),
|
||||
# 'private' parameters: not remotely accessible
|
||||
'heaterrange_pv': PARAM('EPICS pv_name of heater range',
|
||||
datatype=StringType(), default="unset", export=False,),
|
||||
}
|
||||
|
||||
def read_target(self, maxage=0):
|
||||
return self._read_pv(self.target_pv)
|
||||
|
||||
def write_target(self, write_value):
|
||||
# send target to HW
|
||||
self._write_pv(self.target_pv, write_value)
|
||||
# update our status
|
||||
self.read_status()
|
||||
|
||||
def read_value(self, maxage=0):
|
||||
return self._read_pv(self.value_pv)
|
||||
|
||||
def read_status(self, maxage=0):
|
||||
# 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')
|
||||
|
||||
# TODO: add support for strings over epics pv
|
||||
# def read_heaterrange(self, maxage=0):
|
||||
# return self._read_pv(self.heaterrange_pv)
|
||||
|
||||
# TODO: add support for strings over epics pv
|
||||
# def write_heaterrange(self, range_value):
|
||||
# self._write_pv(self.heaterrange_pv, range_value)
|
@ -1,77 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- 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>
|
||||
# *****************************************************************************
|
||||
"""testing devices"""
|
||||
|
||||
import random
|
||||
|
||||
from secop.devices.core import Readable, Driveable, PARAM
|
||||
from secop.datatypes import FloatRange, StringType
|
||||
|
||||
class LN2(Readable):
|
||||
"""Just a readable.
|
||||
|
||||
class name indicates it to be a sensor for LN2,
|
||||
but the implementation may do anything
|
||||
"""
|
||||
|
||||
def read_value(self, maxage=0):
|
||||
return round(100 * random.random(), 1)
|
||||
|
||||
|
||||
class Heater(Driveable):
|
||||
"""Just a driveable.
|
||||
|
||||
class name indicates it to be some heating element,
|
||||
but the implementation may do anything
|
||||
"""
|
||||
PARAMS = {
|
||||
'maxheaterpower': PARAM('maximum allowed heater power',
|
||||
datatype=FloatRange(0, 100), unit='W',
|
||||
),
|
||||
}
|
||||
|
||||
def read_value(self, maxage=0):
|
||||
return round(100 * random.random(), 1)
|
||||
|
||||
def write_target(self, target):
|
||||
pass
|
||||
|
||||
|
||||
class Temp(Driveable):
|
||||
"""Just a driveable.
|
||||
|
||||
class name indicates it to be some temperature controller,
|
||||
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',
|
||||
),
|
||||
}
|
||||
|
||||
def read_value(self, maxage=0):
|
||||
return round(100 * random.random(), 1)
|
||||
|
||||
def write_target(self, target):
|
||||
pass
|
@ -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
|
||||
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)
|
||||
|
@ -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?
|
||||
|
@ -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
|
||||
|
Reference in New Issue
Block a user