implement SECoP proxy modules

A proxy module is a module with a known structure, but
accessed over a SECoP connection.
For the configuration, a Frappy module class has to be given.
The proxy class is created from this, but does not inherit from it.
However, the class of the returned object will be subclass of the
SECoP base classes (Readable, Drivable etc.).
A possible extension might be, that instead of the Frappy class,
the JSON module description can be given, as a separate file
or directly in the config file.
Or we might offer a tool to convert the JSON description to
a python class.

Change-Id: I9212d9f3fe82ec56dfc08611d0e1efc0b0112271
Reviewed-on: https://forge.frm2.tum.de/review/c/sine2020/secop/playground/+/22386
Tested-by: JenkinsCodeReview <bjoern_pedersen@frm2.tum.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
This commit is contained in:
2020-01-30 10:24:40 +01:00
parent 9825b9c135
commit 97034fb998
12 changed files with 550 additions and 114 deletions

View File

@ -26,6 +26,7 @@
import sys
import math
from base64 import b64decode, b64encode
from secop.errors import ProgrammingError, ProtocolError, BadValueError, ConfigError
@ -36,8 +37,8 @@ from secop.properties import HasProperties, Property
# Only export these classes for 'from secop.datatypes import *'
__all__ = [
'DataType',
'FloatRange', 'IntRange',
'DataType', 'get_datatype',
'FloatRange', 'IntRange', 'ScaledInteger',
'BoolType', 'EnumType',
'BLOBType', 'StringType',
'TupleOf', 'ArrayOf', 'StructOf',
@ -51,6 +52,7 @@ UNLIMITED = 1 << 64 # internal limit for integers, is probably high enough for
Parser = Parser()
# base class for all DataTypes
class DataType(HasProperties):
IS_COMMAND = False
@ -116,6 +118,14 @@ class DataType(HasProperties):
# looks like the simplest way to make a deep copy
return get_datatype(self.export_datatype())
def compatible(self, other):
"""check other for compatibility
raise an exception if <other> is not compatible, i.e. there
exists a value which is valid for ourselfs, but not for <other>
"""
raise NotImplementedError
class Stub(DataType):
"""incomplete datatype, to be replaced with a proper one later during module load
@ -182,6 +192,9 @@ class FloatRange(DataType):
value = float(value)
except Exception:
raise BadValueError('Can not __call__ %r to float' % value)
if math.isinf(value):
raise BadValueError('FloatRange does not accept infinity')
prec = max(abs(value * self.relative_resolution), self.absolute_resolution)
if self.min - prec <= value <= self.max + prec:
return min(max(value, self.min), self.max)
@ -215,6 +228,12 @@ class FloatRange(DataType):
return ' '.join([self.fmtstr % value, unit])
return self.fmtstr % value
def compatible(self, other):
if not isinstance(other, (FloatRange, ScaledInteger)):
raise BadValueError('incompatible datatypes')
# avoid infinity
other(max(sys.float_info.min, self.min))
other(min(sys.float_info.max, self.max))
class IntRange(DataType):
@ -266,6 +285,15 @@ class IntRange(DataType):
def format_value(self, value, unit=None):
return '%d' % value
def compatible(self, other):
if isinstance(other, IntRange):
other(self.min)
other(self.max)
return
# this will accept some EnumType, BoolType
for i in range(self.min, self.max + 1):
other(i)
class ScaledInteger(DataType):
"""Scaled integer int type
@ -365,6 +393,12 @@ class ScaledInteger(DataType):
return ' '.join([self.fmtstr % value, unit])
return self.fmtstr % value
def compatible(self, other):
if not isinstance(other, (FloatRange, ScaledInteger)):
raise BadValueError('incompatible datatypes')
other(self.min)
other(self.max)
class EnumType(DataType):
@ -408,6 +442,10 @@ class EnumType(DataType):
def format_value(self, value, unit=None):
return '%s<%s>' % (self._enum[value].name, self._enum[value].value)
def compatible(self, other):
for m in self._enum.members:
other(m)
class BLOBType(DataType):
properties = {
@ -438,7 +476,7 @@ class BLOBType(DataType):
def __call__(self, value):
"""return the validated (internal) value or raise"""
if not isinstance(value, bytes):
raise BadValueError('%r has the wrong type!' % value)
raise BadValueError('%s has the wrong type!' % repr(value))
size = len(value)
if size < self.minbytes:
raise BadValueError(
@ -464,6 +502,13 @@ class BLOBType(DataType):
def format_value(self, value, unit=None):
return repr(value)
def compatible(self, other):
try:
if self.minbytes < other.minbytes or self.maxbytes > other.maxbytes:
raise BadValueError('incompatible datatypes')
except AttributeError:
raise BadValueError('incompatible datatypes')
class StringType(DataType):
properties = {
@ -494,7 +539,7 @@ class StringType(DataType):
def __call__(self, value):
"""return the validated (internal) value or raise"""
if not isinstance(value, str):
raise BadValueError('%r has the wrong type!' % value)
raise BadValueError('%s has the wrong type!' % repr(value))
if not self.isUTF8:
try:
value.encode('ascii')
@ -527,6 +572,14 @@ class StringType(DataType):
def format_value(self, value, unit=None):
return repr(value)
def compatible(self, other):
try:
if self.minchars < other.minchars or self.maxchars > other.maxchars or \
self.isUTF8 > other.isUTF8:
raise BadValueError('incompatible datatypes')
except AttributeError:
raise BadValueError('incompatible datatypes')
# TextType is a special StringType intended for longer texts (i.e. embedding \n),
# whereas StringType is supposed to not contain '\n'
@ -578,6 +631,11 @@ class BoolType(DataType):
def format_value(self, value, unit=None):
return repr(bool(value))
def compatible(self, other):
other(False)
other(True)
Stub.fix_datatypes()
#
@ -673,6 +731,14 @@ class ArrayOf(DataType):
return ' '.join([res, unit])
return res
def compatible(self, other):
try:
if self.minlen < other.minlen or self.maxlen > other.maxlen:
raise BadValueError('incompatible datatypes')
self.members.compatible(other.members)
except AttributeError:
raise BadValueError('incompatible datatypes')
class TupleOf(DataType):
@ -729,6 +795,15 @@ class TupleOf(DataType):
return '(%s)' % (', '.join([sub.format_value(elem)
for sub, elem in zip(self.members, value)]))
def compatible(self, other):
if not isinstance(other, TupleOf):
raise BadValueError('incompatible datatypes')
if len(self.members) != len(other.members) :
raise BadValueError('incompatible datatypes')
for a, b in zip(self.members, other.members):
a.compatible(b)
class StructOf(DataType):
@ -763,7 +838,7 @@ class StructOf(DataType):
return res
def __repr__(self):
opt = self.optional if self.optional else ''
opt = ', optional=%r' % self.optional if self.optional else ''
return 'StructOf(%s%s)' % (', '.join(
['%s=%s' % (n, repr(st)) for n, st in list(self.members.items())]), opt)
@ -808,6 +883,17 @@ class StructOf(DataType):
def format_value(self, value, unit=None):
return '{%s}' % (', '.join(['%s=%s' % (k, self.members[k].format_value(v)) for k, v in sorted(value.items())]))
def compatible(self, other):
try:
mandatory = set(other.members) - set(other.optional)
for k, m in self.members.items():
m.compatible(other.members[k])
mandatory.discard(k)
if mandatory:
raise BadValueError('incompatible datatypes')
except (AttributeError, TypeError, KeyError):
raise BadValueError('incompatible datatypes')
class CommandType(DataType):
IS_COMMAND = True
@ -858,6 +944,16 @@ class CommandType(DataType):
# actually I have no idea what to do here!
raise NotImplementedError
def compatible(self, other):
try:
if self.argument != other.argument: # not both are None
self.argument.compatible(other.argument)
if self.result != other.result: # not both are None
other.result.compatible(self.result)
except AttributeError:
raise BadValueError('incompatible datatypes')
# internally used datatypes (i.e. only for programming the SEC-node)
class DataTypeType(DataType):