several improvements and bugfixes

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

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

View File

@ -156,6 +156,7 @@ class Client(object):
self.log = mlzlog.log.getChild('client', True)
else:
class logStub(object):
def info(self, *args):
pass
debug = info
@ -343,14 +344,17 @@ class Client(object):
def _issueDescribe(self):
_, self.equipment_id, describing_data = self._communicate('describe')
try:
describing_data = self._decode_substruct(['modules'], describing_data)
describing_data = self._decode_substruct(
['modules'], describing_data)
for modname, module in describing_data['modules'].items():
describing_data['modules'][modname] = self._decode_substruct(['parameters', 'commands'], module)
describing_data['modules'][modname] = self._decode_substruct(
['parameters', 'commands'], module)
self.describing_data = describing_data
for module, moduleData in self.describing_data['modules'].items():
for parameter, parameterData in moduleData['parameters'].items():
for parameter, parameterData in moduleData[
'parameters'].items():
datatype = get_datatype(parameterData['datatype'])
self.describing_data['modules'][module]['parameters'] \
[parameter]['datatype'] = datatype

View File

@ -22,12 +22,21 @@
"""Define validated data types."""
from .errors import ProgrammingError
from collections import OrderedDict
# Only export these classes for 'from secop.datatypes import *'
__all__ = [
"DataType",
"FloatRange", "IntRange",
"BoolType", "EnumType",
"BLOBType", "StringType",
"TupleOf", "ArrayOf", "StructOf",
]
# base class for all DataTypes
class DataType(object):
as_json = ['undefined']
@ -39,6 +48,11 @@ class DataType(object):
"""returns a python object fit for external serialisation or logging"""
raise NotImplemented
def from_string(self, text):
"""interprets a given string and returns a validated (internal) value"""
# to evaluate values from configfiles, etc...
raise NotImplemented
# goodie: if called, validate
def __call__(self, value):
return self.validate(value)
@ -52,7 +66,7 @@ class FloatRange(DataType):
self.max = None if max is None else float(max)
# note: as we may compare to Inf all comparisons would be false
if (self.min or float('-inf')) <= (self.max or float('+inf')):
if min == None and max == None:
if min is None and max is None:
self.as_json = ['double']
else:
self.as_json = ['double', min, max]
@ -65,9 +79,11 @@ class FloatRange(DataType):
except:
raise ValueError('Can not validate %r to float' % value)
if self.min is not None and value < self.min:
raise ValueError('%r should not be less then %s' % (value, self.min))
raise ValueError('%r should not be less then %s' %
(value, self.min))
if self.max is not None and value > self.max:
raise ValueError('%r should not be greater than %s' % (value, self.max))
raise ValueError('%r should not be greater than %s' %
(value, self.max))
if None in (self.min, self.max):
return value
if self.min <= value <= self.max:
@ -76,9 +92,10 @@ class FloatRange(DataType):
(value, self.min, self.max))
def __repr__(self):
if self.max != None:
return "FloatRange(%r, %r)" % (float('-inf') if self.min is None else self.min, self.max)
if self.min != None:
if self.max is not None:
return "FloatRange(%r, %r)" % (
float('-inf') if self.min is None else self.min, self.max)
if self.min is not None:
return "FloatRange(%r)" % self.min
return "FloatRange()"
@ -86,15 +103,20 @@ class FloatRange(DataType):
"""returns a python object fit for serialisation"""
return float(value)
def from_string(self, text):
value = float(text)
return self.validate(value)
class IntRange(DataType):
"""Restricted int type"""
def __init__(self, min=None, max=None):
self.min = int(min) if min is not None else min
self.max = int(max) if max is not None else max
if self.min is not None and self.max is not None and self.min > self.max:
raise ValueError('Max must be larger then min!')
if self.min == None and self.max == None:
if self.min is None and self.max is None:
self.as_json = ['int']
else:
self.as_json = ['int', self.min, self.max]
@ -104,10 +126,10 @@ class IntRange(DataType):
value = int(value)
if self.min is not None and value < self.min:
raise ValueError('%r should be an int between %d and %d' %
(value, self.min, self.max or 0))
(value, self.min, self.max or 0))
if self.max is not None and value > self.max:
raise ValueError('%r should be an int between %d and %d' %
(value, self.min or 0, self.max))
(value, self.min or 0, self.max))
return value
except:
raise ValueError('Can not validate %r to int' % value)
@ -123,15 +145,20 @@ class IntRange(DataType):
"""returns a python object fit for serialisation"""
return int(value)
def from_string(self, text):
value = int(text)
return self.validate(value)
class EnumType(DataType):
as_json = ['enum']
def __init__(self, *args, **kwds):
# enum keys are ints! check
self.entries = {}
num = 0
for arg in args:
if type(arg) != str:
if not isinstance(arg, str):
print arg, type(arg)
raise ValueError('EnumType entries MUST be strings!')
self.entries[num] = arg
@ -139,19 +166,24 @@ class EnumType(DataType):
for k, v in kwds.items():
v = int(v)
if v in self.entries:
raise ValueError('keyword argument %r=%d is already assigned %r', k, v, self.entries[v])
raise ValueError(
'keyword argument %r=%d is already assigned %r',
k,
v,
self.entries[v])
self.entries[v] = k
if len(self.entries) == 0:
raise ValueError('Empty enums ae not allowed!')
self.reversed = {}
for k,v in self.entries.items():
for k, v in self.entries.items():
if v in self.reversed:
raise ValueError('Mapping for %r=%r is not Unique!', v, k)
self.reversed[v] = k
self.as_json = ['enum', self.reversed.copy()]
def __repr__(self):
return "EnumType(%s)" % ', '.join(['%s=%d' % (v,k) for k,v in self.entries.items()])
return "EnumType(%s)" % ', '.join(
['%s=%d' % (v, k) for k, v in self.entries.items()])
def export(self, value):
"""returns a python object fit for serialisation"""
@ -159,7 +191,8 @@ class EnumType(DataType):
return self.reversed[value]
if int(value) in self.entries:
return int(value)
raise ValueError('%r is not one of %s', str(value), ', '.join(self.reversed.keys()))
raise ValueError('%r is not one of %s', str(
value), ', '.join(self.reversed.keys()))
def validate(self, value):
"""return the validated (internal) value or raise"""
@ -167,10 +200,16 @@ class EnumType(DataType):
return value
if int(value) in self.entries:
return self.entries[int(value)]
raise ValueError('%r is not one of %s', str(value), ', '.join(map(str,self.entries.keys())))
raise ValueError('%r is not one of %s', str(value),
', '.join(map(str, self.entries.keys())))
def from_string(self, text):
value = text
return self.validate(value)
class BLOBType(DataType):
def __init__(self, minsize=0, maxsize=None):
self.minsize = minsize
self.maxsize = maxsize
@ -194,19 +233,26 @@ class BLOBType(DataType):
raise ValueError('%r has the wrong type!', value)
size = len(value)
if size < self.minsize:
raise ValueError('%r must be at least %d bytes long!', value, self.minsize)
raise ValueError(
'%r must be at least %d bytes long!', value, self.minsize)
if self.maxsize is not None:
if size > self.maxsize:
raise ValueError('%r must be at most %d bytes long!', value, self.maxsize)
raise ValueError(
'%r must be at most %d bytes long!', value, self.maxsize)
return value
def export(self, value):
"""returns a python object fit for serialisation"""
return b'%s' % value
def from_string(self, text):
value = text
return self.validate(value)
class StringType(DataType):
as_json = ['string']
def __init__(self, minsize=0, maxsize=None):
self.minsize = minsize
self.maxsize = maxsize
@ -219,7 +265,8 @@ class StringType(DataType):
def __repr__(self):
if self.maxsize:
return 'StringType(%s, %s)' % (str(self.minsize), str(self.maxsize))
return 'StringType(%s, %s)' % (
str(self.minsize), str(self.maxsize))
if self.minsize:
return 'StringType(%d)' % str(self.minsize)
return 'StringType()'
@ -230,22 +277,31 @@ class StringType(DataType):
raise ValueError('%r has the wrong type!', value)
size = len(value)
if size < self.minsize:
raise ValueError('%r must be at least %d bytes long!', value, self.minsize)
raise ValueError(
'%r must be at least %d bytes long!', value, self.minsize)
if self.maxsize is not None:
if size > self.maxsize:
raise ValueError('%r must be at most %d bytes long!', value, self.maxsize)
raise ValueError(
'%r must be at most %d bytes long!', value, self.maxsize)
if '\0' in value:
raise ValueError('Strings are not allowed to embed a \\0! Use a Blob instead!')
raise ValueError(
'Strings are not allowed to embed a \\0! Use a Blob instead!')
return value
def export(self, value):
"""returns a python object fit for serialisation"""
return '%s' % value
def from_string(self, text):
value = text
return self.validate(value)
# Bool is a special enum
class BoolType(DataType):
as_json = ['bool']
def __repr__(self):
return 'BoolType()'
@ -261,23 +317,31 @@ class BoolType(DataType):
"""returns a python object fit for serialisation"""
return True if self.validate(value) else False
def from_string(self, text):
value = text
return self.validate(value)
#
# nested types
#
class ArrayOf(DataType):
def __init__(self, subtype, minsize_or_size=None, maxsize=None):
if maxsize is None:
maxsize = minsize_or_size
self.minsize = minsize_or_size
self.maxsize = maxsize
if self.minsize is not None and self.maxsize is not None and \
self.minsize > self.maxsize:
raise ValueError('minsize must be less than or equal to maxsize!')
self.minsize > self.maxsize:
raise ValueError('minsize must be less than or equal to maxsize!')
if not isinstance(subtype, DataType):
raise ValueError('ArrayOf only works with DataType objs as first argument!')
raise ValueError(
'ArrayOf only works with DataType objs as first argument!')
self.subtype = subtype
self.as_json = ['array', self.subtype.as_json, self.minsize, self.maxsize]
self.as_json = ['array', self.subtype.as_json,
self.minsize, self.maxsize]
if self.minsize is not None and self.minsize < 0:
raise ValueError('Minimum size must be >= 0!')
if self.maxsize is not None and self.maxsize < 1:
@ -286,32 +350,43 @@ class ArrayOf(DataType):
raise ValueError('Maximum size must be >= Minimum size')
def __repr__(self):
return 'ArrayOf(%s, %s, %s)' % (repr(self.subtype), self.minsize, self.maxsize)
return 'ArrayOf(%s, %s, %s)' % (
repr(self.subtype), self.minsize, self.maxsize)
def validate(self, value):
"""validate a external representation to an internal one"""
if isinstance(value, (tuple, list)):
# check number of elements
if self.minsize is not None and len(value) < self.minsize:
raise ValueError('Array too small, needs at least %d elements!', self.minsize)
raise ValueError(
'Array too small, needs at least %d elements!',
self.minsize)
if self.maxsize is not None and len(value) > self.maxsize:
raise ValueError('Array too big, holds at most %d elements!', self.minsize)
raise ValueError(
'Array too big, holds at most %d elements!', self.minsize)
# apply subtype valiation to all elements and return as list
return [self.subtype.validate(elem) for elem in value]
raise ValueError('Can not convert %s to ArrayOf DataType!', repr(value))
raise ValueError(
'Can not convert %s to ArrayOf DataType!', repr(value))
def export(self, value):
"""returns a python object fit for serialisation"""
return [self.subtype.export(elem) for elem in value]
def from_string(self, text):
value = eval(text) # XXX: !!!
return self.validate(value)
class TupleOf(DataType):
def __init__(self, *subtypes):
if not subtypes:
raise ValueError('Empty tuples are not allowed!')
for subtype in subtypes:
if not isinstance(subtype, DataType):
raise ValueError('TupleOf only works with DataType objs as arguments!')
raise ValueError(
'TupleOf only works with DataType objs as arguments!')
self.subtypes = subtypes
self.as_json = ['tuple', [subtype.as_json for subtype in subtypes]]
@ -323,65 +398,84 @@ class TupleOf(DataType):
# keep the ordering!
try:
if len(value) != len(self.subtypes):
raise ValueError('Illegal number of Arguments! Need %d arguments.', len(self.subtypes))
raise ValueError(
'Illegal number of Arguments! Need %d arguments.', len(
self.subtypes))
# validate elements and return as list
return [sub.validate(elem) for sub,elem in zip(self.subtypes, value)]
return [sub.validate(elem)
for sub, elem in zip(self.subtypes, value)]
except Exception as exc:
raise ValueError('Can not validate:', str(exc))
def export(self, value):
"""returns a python object fit for serialisation"""
return [sub.export(elem) for sub,elem in zip(self.subtypes, value)]
return [sub.export(elem) for sub, elem in zip(self.subtypes, value)]
def from_string(self, text):
value = eval(text) # XXX: !!!
return self.validate(tuple(value))
class StructOf(DataType):
def __init__(self, **named_subtypes):
if not named_subtypes:
raise ValueError('Empty structs are not allowed!')
for name, subtype in named_subtypes.items():
if not isinstance(subtype, DataType):
raise ProgrammingError('StructOf only works with named DataType objs as keyworded arguments!')
raise ProgrammingError(
'StructOf only works with named DataType objs as keyworded arguments!')
if not isinstance(name, (str, unicode)):
raise ProgrammingError('StructOf only works with named DataType objs as keyworded arguments!')
raise ProgrammingError(
'StructOf only works with named DataType objs as keyworded arguments!')
self.named_subtypes = named_subtypes
self.as_json = ['struct', dict((n,s.as_json) for n,s in named_subtypes.items())]
self.as_json = ['struct', dict((n, s.as_json)
for n, s in named_subtypes.items())]
def __repr__(self):
return 'StructOf(%s)' % ', '.join(['%s=%s'%(n,repr(st)) for n,st in self.named_subtypes.iteritems()])
return 'StructOf(%s)' % ', '.join(
['%s=%s' % (n, repr(st)) for n, st in self.named_subtypes.iteritems()])
def validate(self, value):
"""return the validated value or raise"""
try:
if len(value.keys()) != len(self.named_subtypes.keys()):
raise ValueError('Illegal number of Arguments! Need %d arguments.', len(self.namd_subtypes.keys()))
raise ValueError(
'Illegal number of Arguments! Need %d arguments.', len(
self.namd_subtypes.keys()))
# validate elements and return as dict
return dict((str(k), self.named_subtypes[k].validate(v))
for k,v in value.items())
for k, v in value.items())
except Exception as exc:
raise ValueError('Can not validate %s: %s', repr(value),str(exc))
raise ValueError('Can not validate %s: %s', repr(value), str(exc))
def export(self, value):
"""returns a python object fit for serialisation"""
if len(value.keys()) != len(self.named_subtypes.keys()):
raise ValueError('Illegal number of Arguments! Need %d arguments.', len(self.namd_subtypes.keys()))
return dict((str(k),self.named_subtypes[k].export(v))
for k,v in value.items())
raise ValueError(
'Illegal number of Arguments! Need %d arguments.', len(
self.namd_subtypes.keys()))
return dict((str(k), self.named_subtypes[k].export(v))
for k, v in value.items())
def from_string(self, text):
value = eval(text) # XXX: !!!
return self.validate(dict(value))
# XXX: derive from above classes automagically!
DATATYPES = dict(
bool = lambda : BoolType(),
int = lambda _min=None, _max=None: IntRange(_min, _max),
double = lambda _min=None, _max=None: FloatRange(_min, _max),
blob = lambda _min=None, _max=None: BLOBType(_min, _max),
string = lambda _min=None, _max=None: StringType(_min, _max),
array = lambda subtype, _min=None, _max=None: ArrayOf(get_datatype(subtype), _min, _max),
tuple = lambda subtypes: TupleOf(*map(get_datatype,subtypes)),
enum = lambda kwds: EnumType(**kwds),
struct = lambda named_subtypes: StructOf(**dict((n,get_datatype(t)) for n,t in named_subtypes.items())),
bool=lambda: BoolType(),
int=lambda _min=None, _max=None: IntRange(_min, _max),
double=lambda _min=None, _max=None: FloatRange(_min, _max),
blob=lambda _min=None, _max=None: BLOBType(_min, _max),
string=lambda _min=None, _max=None: StringType(_min, _max),
array=lambda subtype, _min=None, _max=None: ArrayOf(
get_datatype(subtype), _min, _max),
tuple=lambda subtypes: TupleOf(*map(get_datatype, subtypes)),
enum=lambda kwds: EnumType(**kwds),
struct=lambda named_subtypes: StructOf(
**dict((n, get_datatype(t)) for n, t in named_subtypes.items())),
)
@ -392,12 +486,14 @@ def export_datatype(datatype):
return datatype.as_json
# important for getting the right datatype from formerly jsonified descr.
def get_datatype(json):
if json is None:
return json
if not isinstance(json, list):
raise ValueError('Argument must be a properly formatted list!')
if len(json)<1:
if len(json) < 1:
raise ValueError('can not validate %r', json)
base = json[0]
if base in DATATYPES:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -71,7 +71,6 @@ class MainWindow(QMainWindow):
loadUi(self, 'mainwindow.ui')
self.toolBar.hide()
self.lineEdit.hide()
self.splitter.setStretchFactor(0, 1)
self.splitter.setStretchFactor(1, 70)
@ -108,6 +107,13 @@ class MainWindow(QMainWindow):
QMessageBox.critical(self.parent(),
'Connecting to %s failed!' % host, str(e))
def on_validateCheckBox_toggled(self, state):
print "validateCheckBox_toggled", state
def on_visibilityComboBox_activated(self, level):
if level in ['user', 'admin', 'expert']:
print "visibility Level now:", level
def on_treeWidget_currentItemChanged(self, current, previous):
if current.type() == ITEM_TYPE_NODE:
self._displayNode(current.text(0))

View File

@ -56,6 +56,7 @@ class ParameterButtons(QWidget):
class ParameterGroup(QWidget):
def __init__(self, groupname, parent=None):
super(ParameterGroup, self).__init__(parent)
loadUi(self, 'paramgroup.ui')
@ -107,19 +108,17 @@ class ModuleCtrl(QWidget):
self._node.newData.connect(self._updateValue)
def _initModuleWidgets(self):
initValues = self._node.queryCache(self._module)
row = 0
# collect grouping information
paramsByGroup = {} # groupname -> [paramnames]
allGroups = set()
params = self._node.getParameters(self._module)
for param in params:
props = self._node.getProperties(self._module, param)
group = props.get('group',None)
group = props.get('group', None)
if group is not None:
allGroups.add(group)
paramsByGroup.setdefault(group, []).append(param)
@ -139,7 +138,8 @@ class ModuleCtrl(QWidget):
# check if there is a param of the same name too
if group in params:
# yes: create a widget for this as well
labelstr, buttons = self._makeEntry(param, initValues[param].value, nolabel=True, checkbox=checkbox, invert=True)
labelstr, buttons = self._makeEntry(
param, initValues[param].value, nolabel=True, checkbox=checkbox, invert=True)
checkbox.setText(labelstr)
# add to Layout (yes: ignore the label!)
@ -153,7 +153,8 @@ class ModuleCtrl(QWidget):
for param in paramsByGroup[param]:
if param == group:
continue
label, buttons = self._makeEntry(param, initValues[param].value, checkbox=checkbox, invert=False)
label, buttons = self._makeEntry(
param, initValues[param].value, checkbox=checkbox, invert=False)
# add to Layout
self.paramGroupBox.layout().addWidget(label, row, 0)
@ -161,22 +162,29 @@ class ModuleCtrl(QWidget):
row += 1
else:
# param is a 'normal' param: create a widget if it has no group or is named after a group (otherwise its created above)
# param is a 'normal' param: create a widget if it has no group
# or is named after a group (otherwise its created above)
props = self._node.getProperties(self._module, param)
if props.get('group', param) == param:
label, buttons = self._makeEntry(param, initValues[param].value)
label, buttons = self._makeEntry(
param, initValues[param].value)
# add to Layout
self.paramGroupBox.layout().addWidget(label, row, 0)
self.paramGroupBox.layout().addWidget(buttons, row, 1)
row += 1
def _makeEntry(self, param, initvalue, nolabel=False, checkbox=None, invert=False):
def _makeEntry(
self,
param,
initvalue,
nolabel=False,
checkbox=None,
invert=False):
props = self._node.getProperties(self._module, param)
description = props.get('description', '')
unit = props.get('unit','')
unit = props.get('unit', '')
if unit:
labelstr = '%s (%s):' % (param, unit)
@ -186,7 +194,8 @@ class ModuleCtrl(QWidget):
if checkbox and not invert:
labelstr = ' ' + labelstr
buttons = ParameterButtons(self._module, param, initvalue, props['readonly'])
buttons = ParameterButtons(
self._module, param, initvalue, props['readonly'])
buttons.setRequested.connect(self._set_Button_pressed)
if description:
@ -199,7 +208,11 @@ class ModuleCtrl(QWidget):
label.setFont(self._labelfont)
if checkbox:
def stateChanged(newstate, buttons=buttons, label=None if nolabel else label, invert=invert):
def stateChanged(
newstate,
buttons=buttons,
label=None if nolabel else label,
invert=invert):
if (newstate and not invert) or (invert and not newstate):
buttons.show()
if label:
@ -217,9 +230,6 @@ class ModuleCtrl(QWidget):
return label, buttons
def _set_Button_pressed(self, module, parameter, target):
sig = (module, parameter, target)
if self._lastclick == sig:

View File

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

View File

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

View File

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

View File

@ -21,7 +21,34 @@
# *****************************************************************************
"""Define helpers"""
import os
import re
import sys
import errno
import signal
import socket
import fnmatch
import linecache
import threading
import traceback
import subprocess
import unicodedata
from os import path
class lazy_property(object):
"""A property that calculates its value only once."""
def __init__(self, func):
self._func = func
self.__name__ = func.__name__
self.__doc__ = func.__doc__
def __get__(self, obj, obj_class):
if obj is None:
return obj
obj.__dict__[self.__name__] = self._func(obj)
return obj.__dict__[self.__name__]
class attrdict(dict):
@ -48,8 +75,11 @@ def get_class(spec):
"""loads a class given by string in dotted notaion (as python would do)"""
modname, classname = spec.rsplit('.', 1)
import importlib
module = importlib.import_module('secop.' + modname)
# module = __import__(spec)
if modname.startswith('secop'):
module = importlib.import_module(modname)
else:
# rarely needed by now....
module = importlib.import_module('secop.' + modname)
return getattr(module, classname)
@ -64,10 +94,6 @@ def mkthread(func, *args, **kwds):
return t
import sys
import linecache
import traceback
def formatExtendedFrame(frame):
ret = []
for key, value in frame.f_locals.iteritems():
@ -79,6 +105,7 @@ def formatExtendedFrame(frame):
ret.append('\n')
return ret
def formatExtendedTraceback(exc_info=None):
if exc_info is None:
etype, value, tb = sys.exc_info()
@ -101,6 +128,7 @@ def formatExtendedTraceback(exc_info=None):
ret += traceback.format_exception_only(etype, value)
return ''.join(ret).rstrip('\n')
def formatExtendedStack(level=1):
f = sys._getframe(level)
ret = ['Stack trace (most recent call last):\n\n']
@ -120,6 +148,7 @@ def formatExtendedStack(level=1):
f = f.f_back
return ''.join(ret).rstrip('\n')
def formatException(cut=0, exc_info=None, verbose=False):
"""Format an exception with traceback, but leave out the first `cut`
number of frames.
@ -137,6 +166,63 @@ def formatException(cut=0, exc_info=None, verbose=False):
return ''.join(res)
def parseHostPort(host, defaultport):
"""Parse host[:port] string and tuples
Specify 'host[:port]' or a (host, port) tuple for the mandatory argument.
If the port specification is missing, the value of the defaultport is used.
"""
if isinstance(host, (tuple, list)):
host, port = host
elif ':' in host:
host, port = host.rsplit(':', 1)
port = int(port)
else:
port = defaultport
assert 0 < port < 65536
assert ':' not in host
return host, port
def tcpSocket(host, defaultport, timeout=None):
"""Helper for opening a TCP client socket to a remote server.
Specify 'host[:port]' or a (host, port) tuple for the mandatory argument.
If the port specification is missing, the value of the defaultport is used.
If timeout is set to a number, the timout of the connection is set to this
number, else the socket stays in blocking mode.
"""
host, port = parseHostPort(host, defaultport)
# open socket and set options
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
if timeout:
s.settimeout(timeout)
# connect
s.connect((host, int(port)))
return s
def closeSocket(sock, socket=socket):
"""Do our best to close a socket."""
if sock is None:
return
try:
sock.shutdown(socket.SHUT_RDWR)
except socket.error:
pass
try:
sock.close()
except socket.error:
pass
def getfqdn(name=''):
"""Get fully qualified hostname."""
return socket.getfqdn(name)
if __name__ == '__main__':
print "minimal testing: lib"
d = attrdict(a=1, b=2)

View File

@ -56,17 +56,15 @@ class PARAM(object):
unit=None,
readonly=True,
export=True,
group=''):
if isinstance(description, PARAM):
# make a copy of a PARAM object
self.__dict__.update(description.__dict__)
return
group='',
poll=False):
if not isinstance(datatype, DataType):
if issubclass(datatype, DataType):
# goodie: make an instance from a class (forgotten ()???)
datatype = datatype()
else:
raise ValueError('Datatype MUST be from datatypes!')
raise ValueError(
'datatype MUST be derived from class DataType!')
self.description = description
self.datatype = datatype
self.default = default
@ -74,6 +72,9 @@ class PARAM(object):
self.readonly = readonly
self.export = export
self.group = group
# note: auto-converts True/False to 1/0 which yield the expected
# behaviour...
self.poll = int(poll)
# internal caching: value and timestamp of last change...
self.value = default
self.timestamp = 0
@ -82,6 +83,18 @@ class PARAM(object):
return '%s(%s)' % (self.__class__.__name__, ', '.join(
['%s=%r' % (k, v) for k, v in sorted(self.__dict__.items())]))
def copy(self):
# return a copy of ourselfs
return PARAM(description=self.description,
datatype=self.datatype,
default=self.default,
unit=self.unit,
readonly=self.readonly,
export=self.export,
group=self.group,
poll=self.poll,
)
def as_dict(self, static_only=False):
# used for serialisation only
res = dict(
@ -99,11 +112,36 @@ class PARAM(object):
res['timestamp'] = format_time(self.timestamp)
return res
@property
def export_value(self):
return self.datatype.export(self.value)
class OVERRIDE(object):
def __init__(self, **kwds):
self.kwds = kwds
def apply(self, paramobj):
if isinstance(paramobj, PARAM):
for k, v in self.kwds.iteritems():
if hasattr(paramobj, k):
setattr(paramobj, k, v)
return paramobj
else:
raise ProgrammingError(
"Can not apply Override(%s=%r) to %r: non-existing property!" %
(k, v, paramobj))
else:
raise ProgrammingError(
"Overrides can only be applied to PARAM's, %r is none!" %
paramobj)
# storage for CMDs settings (description + call signature...)
class CMD(object):
def __init__(self, description, arguments, result):
def __init__(self, description, arguments=[], result=None):
# descriptive text for humans
self.description = description
# list of datatypes for arguments
@ -122,10 +160,10 @@ class CMD(object):
arguments=map(export_datatype, self.arguments),
resulttype=export_datatype(self.resulttype), )
# Meta class
# warning: MAGIC!
class DeviceMeta(type):
def __new__(mcs, name, bases, attrs):
@ -142,37 +180,61 @@ class DeviceMeta(type):
newentry.update(attrs.get(entry, {}))
setattr(newtype, entry, newentry)
# apply Overrides from all sub-classes
newparams = getattr(newtype, 'PARAMS')
for base in reversed(bases):
overrides = getattr(base, 'OVERRIDES', {})
for n, o in overrides.iteritems():
newparams[n] = o.apply(newparams[n].copy())
for n, o in attrs.get('OVERRIDES', {}).iteritems():
newparams[n] = o.apply(newparams[n].copy())
# check validity of PARAM entries
for pname, pobj in newtype.PARAMS.items():
# XXX: allow dicts for overriding certain aspects only.
if not isinstance(pobj, PARAM):
raise ProgrammingError('%r: device PARAM %r should be a '
raise ProgrammingError('%r: PARAMs entry %r should be a '
'PARAM object!' % (name, pname))
# XXX: create getters for the units of params ??
# wrap of reading/writing funcs
rfunc = attrs.get('read_' + pname, None)
for base in bases:
if rfunc is not None:
break
rfunc = getattr(base, 'read_' + pname, None)
def wrapped_rfunc(self, maxage=0, pname=pname, rfunc=rfunc):
if rfunc:
self.log.debug("rfunc(%s): call %r" % (pname, rfunc))
value = rfunc(self, maxage)
setattr(self, pname, value)
return value
else:
# return cached value
self.log.debug("rfunc(%s): return cached value" % pname)
return self.PARAMS[pname].value
if rfunc:
wrapped_rfunc.__doc__ = rfunc.__doc__
setattr(newtype, 'read_' + pname, wrapped_rfunc)
if getattr(rfunc, '__wrapped__', False) == False:
setattr(newtype, 'read_' + pname, wrapped_rfunc)
wrapped_rfunc.__wrapped__ = True
if not pobj.readonly:
wfunc = attrs.get('write_' + pname, None)
for base in bases:
if wfunc is not None:
break
wfunc = getattr(base, 'write_' + pname, None)
def wrapped_wfunc(self, value, pname=pname, wfunc=wfunc):
self.log.debug("wfunc: set %s to %r" % (pname, value))
self.log.debug("wfunc(%s): set %r" % (pname, value))
pobj = self.PARAMS[pname]
value = pobj.datatype.validate(value) if pobj.datatype else value
value = pobj.datatype.validate(value)
if wfunc:
self.log.debug('calling %r(%r)' % (wfunc, value))
value = wfunc(self, value) or value
# XXX: use setattr or direct manipulation
# of self.PARAMS[pname]?
@ -181,14 +243,16 @@ class DeviceMeta(type):
if wfunc:
wrapped_wfunc.__doc__ = wfunc.__doc__
setattr(newtype, 'write_' + pname, wrapped_wfunc)
if getattr(wfunc, '__wrapped__', False) == False:
setattr(newtype, 'write_' + pname, wrapped_wfunc)
wrapped_wfunc.__wrapped__ = True
def getter(self, pname=pname):
return self.PARAMS[pname].value
def setter(self, value, pname=pname):
pobj = self.PARAMS[pname]
value = pobj.datatype.validate(value) if pobj.datatype else value
value = pobj.datatype.validate(value)
pobj.timestamp = time.time()
if not EVENT_ONLY_ON_CHANGED_VALUES or (value != pobj.value):
pobj.value = value
@ -251,13 +315,7 @@ class Device(object):
# make local copies of PARAMS
params = {}
for k, v in self.PARAMS.items()[:]:
#params[k] = PARAM(v)
# PARAM: type(v) -> PARAM
# type(v)(v) -> PARAM(v)
# EPICS_PARAM: type(v) -> EPICS_PARAM
# type(v)(v) -> EPICS_PARAM(v)
param_type = type(v)
params[k] = param_type(v)
params[k] = v.copy()
self.PARAMS = params
@ -273,13 +331,13 @@ class Device(object):
mycls = self.__class__
myclassname = '%s.%s' % (mycls.__module__, mycls.__name__)
self.PROPERTIES['implementation'] = myclassname
self.PROPERTIES['interfaces'] = [b.__name__ for b in mycls.__mro__
if b.__module__.startswith('secop.devices.core')]
self.PROPERTIES['interfaces'] = [
b.__name__ for b in mycls.__mro__ if b.__module__.startswith('secop.modules')]
self.PROPERTIES['interface'] = self.PROPERTIES['interfaces'][0]
# remove unset (default) module properties
for k, v in self.PROPERTIES.items():
if v == None:
if v is None:
del self.PROPERTIES[k]
# check and apply parameter_properties
@ -312,13 +370,13 @@ class Device(object):
cfgdict[k] = v.default
# replace CLASS level PARAM objects with INSTANCE level ones
#self.PARAMS[k] = PARAM(self.PARAMS[k])
param_type = type(self.PARAMS[k])
self.PARAMS[k] = param_type(self.PARAMS[k])
self.PARAMS[k] = self.PARAMS[k].copy()
# now 'apply' config:
# pass values through the datatypes and store as attributes
for k, v in cfgdict.items():
if k == 'value':
continue
# apply datatype, complain if type does not fit
datatype = self.PARAMS[k].datatype
if datatype is not None:
@ -334,11 +392,7 @@ class Device(object):
def init(self):
# may be overriden in derived classes to init stuff
self.log.debug('init()')
def _pollThread(self):
# may be overriden in derived classes to init stuff
self.log.debug('init()')
self.log.debug('empty init()')
class Readable(Device):
@ -348,7 +402,7 @@ class Readable(Device):
"""
PARAMS = {
'value': PARAM('current value of the device', readonly=True, default=0.,
datatype=FloatRange()),
datatype=FloatRange(), poll=True),
'pollinterval': PARAM('sleeptime between polls', default=5,
readonly=False, datatype=FloatRange(0.1, 120), ),
'status': PARAM('current status of the device', default=(status.OK, ''),
@ -360,25 +414,38 @@ class Readable(Device):
'UNSTABLE': status.UNSTABLE,
'ERROR': status.ERROR,
'UNKNOWN': status.UNKNOWN
}), StringType() ),
readonly=True),
}), StringType()),
readonly=True, poll=True),
}
def init(self):
Device.init(self)
self._pollthread = threading.Thread(target=self._pollThread)
self._pollthread = threading.Thread(target=self.__pollThread)
self._pollthread.daemon = True
self._pollthread.start()
def _pollThread(self):
def __pollThread(self):
"""super simple and super stupid per-module polling thread"""
i = 0
while True:
time.sleep(self.pollinterval)
for pname in self.PARAMS:
if pname != 'pollinterval':
rfunc = getattr(self, 'read_%s' % pname, None)
if rfunc:
rfunc()
i = 1
try:
time.sleep(self.pollinterval)
except TypeError:
time.sleep(max(self.pollinterval))
try:
self.poll(i)
except Exception: # really ALL
pass
def poll(self, nr):
for pname, pobj in self.PARAMS.iteritems():
if not pobj.poll:
continue
if 0 == nr % int(pobj.poll):
rfunc = getattr(self, 'read_' + pname, None)
if rfunc:
rfunc()
class Driveable(Readable):
@ -387,9 +454,12 @@ class Driveable(Readable):
providing a settable 'target' parameter to those of a Readable
"""
PARAMS = {
'target': PARAM('target value of the device', default=0., readonly=False,
datatype=FloatRange(),
),
'target': PARAM(
'target value of the device',
default=0.,
readonly=False,
datatype=FloatRange(),
),
}
# XXX: CMDS ???? auto deriving working well enough?

View File

@ -96,8 +96,8 @@ class Dispatcher(object):
except Exception as err:
self.log.exception(err)
reply = msg.get_error(
errorclass='InternalError',
errorinfo=[formatException(), str(msg), formatExtendedStack()])
errorclass='InternalError', errorinfo=[
formatException(), str(msg), formatExtendedStack()])
else:
self.log.debug('Can not handle msg %r' % msg)
reply = self.unhandled(conn, msg)
@ -125,7 +125,7 @@ class Dispatcher(object):
msg = Value(
moduleobj.name,
parameter=pname,
value=pobj.value,
value=pobj.export_value,
t=pobj.timestamp)
self.broadcast_event(msg)
@ -215,8 +215,9 @@ class Dispatcher(object):
for modulename in self._export:
module = self.get_module(modulename)
# some of these need rework !
mod_desc = {'parameters':[], 'commands':[]}
for pname, param in self.list_module_params(modulename, only_static=True).items():
mod_desc = {'parameters': [], 'commands': []}
for pname, param in self.list_module_params(
modulename, only_static=True).items():
mod_desc['parameters'].extend([pname, param])
for cname, cmd in self.list_module_cmds(modulename).items():
mod_desc['commands'].extend([cname, cmd])
@ -236,7 +237,9 @@ class Dispatcher(object):
module = self.get_module(modulename)
# some of these need rework !
dd = {
'parameters': self.list_module_params(modulename, only_static=True),
'parameters': self.list_module_params(
modulename,
only_static=True),
'commands': self.list_module_cmds(modulename),
'properties': module.PROPERTIES,
}
@ -319,9 +322,9 @@ class Dispatcher(object):
return Value(
modulename,
parameter=pname,
value=pobj.value,
value=pobj.export_value,
t=pobj.timestamp)
return Value(modulename, parameter=pname, value=pobj.value)
return Value(modulename, parameter=pname, value=pobj.export_value)
# now the (defined) handlers for the different requests
def handle_Help(self, conn, msg):
@ -396,7 +399,7 @@ class Dispatcher(object):
res = Value(
module=modulename,
parameter=pname,
value=pobj.value,
value=pobj.export_value,
t=pobj.timestamp,
unit=pobj.unit)
if res.value != Ellipsis: # means we do not have a value at all so skip this