# ***************************************************************************** # # 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 # # ***************************************************************************** """SECoP proxy modules""" from frappy.client import SecopClient, decode_msg, encode_msg_frame from frappy.datatypes import StringType from frappy.errors import BadValueError, CommunicationFailedError, ConfigError from frappy.lib import get_class from frappy.modules import Drivable, Module, Readable, Writable from frappy.params import Command, Parameter from frappy.properties import Property from frappy.io import HasIO DISCONNECTED = Readable.Status.ERROR, 'disconnected' class Proxy(HasIO, Module): module = Property('remote module name', datatype=StringType(), default='') status = Parameter('connection status', Readable.status.datatype) # add status even when not a Readable _consistency_check_done = False _connection_status = None # status when not connected _secnode = None enablePoll = False def __new__(cls, name, logger, cfgdict, srv): """create a Proxy class based on remote_class""" remote_class = cfgdict.pop('remote_class') if isinstance(remote_class, dict): remote_class = remote_class['value'] if 'description' not in cfgdict: cfgdict['description'] = (f"remote module {cfgdict.get('module', name)} " f"on {cfgdict.get('io', {'value:': '?'})['value']}") proxycls = proxy_class(remote_class) return super().__new__(proxycls, name, logger, cfgdict, srv) def ioClass(self, name, logger, opts, srv): opts['description'] = f"secnode {opts.get('module', name)} on {opts['uri']}" return SecNode(name, logger, opts, srv) def updateEvent(self, module, parameter, value, timestamp, readerror): if parameter not in self.parameters: return # ignore unknown parameters if parameter == 'status' and not readerror: self._connection_status = None # should be done here: deal with clock differences self.announceUpdate(parameter, value, readerror, timestamp) def initModule(self): if not self.module: self.module = self.name self._secnode = self.io.secnode self._secnode.register_callback(self.module, self.updateEvent, self.descriptiveDataChange, self.nodeStateChange) 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: if pobj.export and pname != 'status': 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 BadValueError: self.log.warning('remote command %s:%s is not compatible: %r != %r', self.module, cname, cobj.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: if not self._consistency_check_done: self._check_descriptive_data() self._consistency_check_done = True if self._connection_status: self.status = Readable.Status.IDLE, state else: readerror = CommunicationFailedError('disconnected') if self.status != DISCONNECTED: for pname in set(self.parameters) - set(('module', 'status')): self.announceUpdate(pname, None, readerror) self.status = self._connection_status = DISCONNECTED def checkProperties(self): pass # skip class ProxyReadable(Proxy, Readable): pass class ProxyWritable(Proxy, Writable): pass class ProxyDrivable(Proxy, Drivable): pass PROXY_CLASSES = [ProxyDrivable, ProxyWritable, ProxyReadable, Proxy] class SecNode(Module): uri = Property('uri of a SEC node', datatype=StringType()) def earlyInit(self): super().earlyInit() self.secnode = SecopClient(self.uri, self.log) def startModule(self, start_events): super().startModule(start_events) self.secnode.spawn_connect(start_events.get_trigger()) @Command(StringType(), result=StringType()) def request(self, msg): """send a request, for debugging purposes""" reply = self.secnode.request(*decode_msg(msg.encode('utf-8'))) # pylint: disable=not-an-iterable 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 . of a class used on the remote node if name is not given, is used """ if isinstance(remote_class, type) and issubclass(remote_class, Module): rcls = remote_class remote_class = rcls.__name__ else: 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(f'{remote_class!r} is no SECoP module class') attrs = rcls.propertyDict.copy() for aname, aobj in rcls.accessibles.items(): if isinstance(aobj, Parameter): pobj = aobj.copy() # we have to set own properties of pobj to the inherited ones # this matters for the ProxyModule.status datatype, which should be # overidden by the remote class status datatype pobj.ownProperties = pobj.propertyValues pobj.merge({'needscfg': False}) attrs[aname] = pobj def rfunc(self, pname=aname): value, _, readerror = self._secnode.getParameter(self.module, pname, True) 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.module, pname, value) if readerror: raise readerror return value attrs['write_' + aname] = wfunc elif isinstance(aobj, Command): cobj = aobj.copy() def cfunc(self, arg=None, cname=aname): return self._secnode.execCommand(self.module, cname, arg)[0] attrs[aname] = cobj(cfunc) else: raise ConfigError(f'do not now about {aobj!r} in {remote_class}.accessibles') return type(name+"_", (proxycls,), attrs)