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>
231 lines
8.1 KiB
Python
231 lines
8.1 KiB
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:
|
|
# Markus Zolliker <markus.zolliker@psi.ch>
|
|
#
|
|
# *****************************************************************************
|
|
"""SECoP proxy modules"""
|
|
|
|
from secop.lib import get_class
|
|
from secop.modules import Module, Writable, Readable, Drivable, Attached
|
|
from secop.datatypes import StringType
|
|
from secop.protocol.dispatcher import make_update
|
|
from secop.properties import Property
|
|
from secop.client import SecopClient, decode_msg, encode_msg_frame
|
|
from secop.params import Parameter, Command
|
|
from secop.errors import ConfigError, make_secop_error, secop_error
|
|
|
|
|
|
|
|
class ProxyModule(Module):
|
|
properties = {
|
|
'iodev': Attached(),
|
|
'module':
|
|
Property('remote module name', datatype=StringType(), default=''),
|
|
}
|
|
|
|
_consistency_check_done = False
|
|
_secnode = None
|
|
|
|
def updateEvent(self, module, parameter, value, timestamp, readerror):
|
|
pobj = self.parameters[parameter]
|
|
pobj.timestamp = timestamp
|
|
# should be done here: deal with clock differences
|
|
if readerror:
|
|
readerror = make_secop_error(*readerror)
|
|
if not readerror:
|
|
try:
|
|
pobj.value = value # store the value even in case of a validation error
|
|
pobj.value = pobj.datatype(value)
|
|
except Exception as e:
|
|
readerror = secop_error(e)
|
|
pobj.readerror = readerror
|
|
self.DISPATCHER.broadcast_event(make_update(self.name, pobj))
|
|
|
|
def initModule(self):
|
|
if not self.module:
|
|
self.properties['module'] = self.name
|
|
self._secnode = self._iodev.secnode
|
|
self._secnode.register(self.module, self)
|
|
super().initModule()
|
|
|
|
def descriptiveDataChange(self, module, moddesc):
|
|
if module is None:
|
|
return # do not care about the node for now
|
|
self._check_descriptive_data()
|
|
|
|
def _check_descriptive_data(self):
|
|
params = self.parameters.copy()
|
|
cmds = self.commands.copy()
|
|
moddesc = self._secnode.modules[self.module]
|
|
remoteparams = moddesc['parameters'].copy()
|
|
remotecmds = moddesc['commands'].copy()
|
|
while params:
|
|
pname, pobj = params.popitem()
|
|
props = remoteparams.get(pname, None)
|
|
if props is None:
|
|
self.log.warning('remote parameter %s:%s does not exist' % (self.module, pname))
|
|
continue
|
|
dt = props['datatype']
|
|
try:
|
|
if pobj.readonly:
|
|
dt.compatible(pobj.datatype)
|
|
else:
|
|
if props['readonly']:
|
|
self.log.warning('remote parameter %s:%s is read only' % (self.module, pname))
|
|
pobj.datatype.compatible(dt)
|
|
try:
|
|
dt.compatible(pobj.datatype)
|
|
except Exception:
|
|
self.log.warning('remote parameter %s:%s is not fully compatible: %r != %r'
|
|
% (self.module, pname, pobj.datatype, dt))
|
|
except Exception:
|
|
self.log.warning('remote parameter %s:%s has an incompatible datatype: %r != %r'
|
|
% (self.module, pname, pobj.datatype, dt))
|
|
while cmds:
|
|
cname, cobj = cmds.popitem()
|
|
props = remotecmds.get(cname)
|
|
if props is None:
|
|
self.log.warning('remote command %s:%s does not exist' % (self.module, cname))
|
|
continue
|
|
dt = props['datatype']
|
|
try:
|
|
cobj.datatype.compatible(dt)
|
|
except Exception:
|
|
self.log.warning('remote command %s:%s is not compatible: %r != %r'
|
|
% (self.module, pname, pobj.datatype, dt))
|
|
# what to do if descriptive data does not match?
|
|
# we might raise an exception, but this would lead to a reconnection,
|
|
# which might not help.
|
|
# for now, the error message must be enough
|
|
|
|
def nodeStateChange(self, online, state):
|
|
if online and not self._consistency_check_done:
|
|
self._check_descriptive_data()
|
|
self._consistency_check_done = True
|
|
|
|
|
|
class ProxyReadable(ProxyModule, Readable):
|
|
pass
|
|
|
|
|
|
class ProxyWritable(ProxyModule, Writable):
|
|
pass
|
|
|
|
|
|
class ProxyDrivable(ProxyModule, Drivable):
|
|
pass
|
|
|
|
|
|
PROXY_CLASSES = [ProxyDrivable, ProxyWritable, ProxyReadable, ProxyModule]
|
|
|
|
|
|
class SecNode(Module):
|
|
properties = {
|
|
'uri':
|
|
Property('uri of a SEC node', datatype=StringType()),
|
|
}
|
|
commands = {
|
|
'request':
|
|
Command('send a request', argument=StringType(), result=StringType())
|
|
}
|
|
|
|
def earlyInit(self):
|
|
self.secnode = SecopClient(self.uri, self.log)
|
|
self.secnode.register(None, self) # for nodeStateChange
|
|
|
|
def startModule(self, started_callback):
|
|
self.secnode.spawn_connect(started_callback)
|
|
|
|
def do_request(self, msg):
|
|
"""for test purposes"""
|
|
reply = self.secnode.request(*decode_msg(msg.encode('utf-8')))
|
|
return encode_msg_frame(*reply).decode('utf-8')
|
|
|
|
|
|
def proxy_class(remote_class, name=None):
|
|
"""create a proxy class based on the definition of remote class
|
|
|
|
remote class is <import path>.<class name> of a class used on the remote node
|
|
if name is not given, 'Proxy' + <class name> is used
|
|
"""
|
|
rcls = get_class(remote_class)
|
|
if name is None:
|
|
name = rcls.__name__
|
|
|
|
for proxycls in PROXY_CLASSES:
|
|
if issubclass(rcls, proxycls.__bases__[-1]):
|
|
# avoid 'should not be redefined' warning
|
|
proxycls.accessibles = {}
|
|
break
|
|
else:
|
|
raise ConfigError('%r is no SECoP module class' % remote_class)
|
|
|
|
parameters = {}
|
|
commands = {}
|
|
attrs = dict(parameters=parameters, commands=commands, properties=rcls.properties)
|
|
|
|
for aname, aobj in rcls.accessibles.items():
|
|
if isinstance(aobj, Parameter):
|
|
pobj = aobj.copy()
|
|
parameters[aname] = pobj
|
|
pobj.properties['poll'] = False
|
|
pobj.properties['handler'] = None
|
|
pobj.properties['needscfg'] = False
|
|
|
|
def rfunc(self, pname=aname):
|
|
value, _, readerror = self._secnode.getParameter(self.name, pname)
|
|
if readerror:
|
|
raise readerror
|
|
return value
|
|
|
|
attrs['read_' + aname] = rfunc
|
|
|
|
if not pobj.readonly:
|
|
|
|
def wfunc(self, value, pname=aname):
|
|
value, _, readerror = self._secnode.setParameter(self.name, pname, value)
|
|
if readerror:
|
|
raise make_secop_error(*readerror)
|
|
return value
|
|
|
|
attrs['write_' + aname] = wfunc
|
|
|
|
elif isinstance(aobj, Command):
|
|
cobj = aobj.copy()
|
|
commands[aname] = cobj
|
|
|
|
def cfunc(self, arg=None, cname=aname):
|
|
return self._secnode.execCommand(self.name, cname, arg)
|
|
|
|
attrs['do_' + aname] = cfunc
|
|
|
|
else:
|
|
raise ConfigError('do not now about %r in %s.accessibles' % (aobj, remote_class))
|
|
|
|
return type(name, (proxycls,), attrs)
|
|
|
|
|
|
def Proxy(name, logger, cfgdict, srv):
|
|
"""create a Proxy object based on remote_class
|
|
|
|
title cased as it acts like a class
|
|
"""
|
|
remote_class = cfgdict.pop('remote_class')
|
|
return proxy_class(remote_class)(name, logger, cfgdict, srv)
|