diff --git a/secop/client/baseclient.py b/secop/client/baseclient.py index f031b7a..dbc5e4b 100644 --- a/secop/client/baseclient.py +++ b/secop/client/baseclient.py @@ -34,12 +34,14 @@ import mlzlog import serial from secop.datatypes import CommandType, EnumType, get_datatype -#from secop.protocol.encoding import ENCODERS -#from secop.protocol.framing import FRAMERS -#from secop.protocol.messages import * from secop.errors import EXCEPTIONS from secop.lib import formatException, formatExtendedStack, mkthread from secop.lib.parsing import format_time, parse_time +from secop.protocol.messages import BUFFERREQUEST, COMMANDREQUEST, \ + DESCRIPTIONREPLY, DESCRIPTIONREQUEST, DISABLEEVENTSREQUEST, \ + ENABLEEVENTSREQUEST, ERRORPREFIX, EVENTREPLY, \ + HEARTBEATREQUEST, HELPREQUEST, IDENTREQUEST, READREPLY, \ + READREQUEST, REQUEST2REPLY, WRITEREPLY, WRITEREQUEST try: # py3 @@ -76,8 +78,8 @@ class TCPConnection(object): try: data = u'' while True: + newdata = b'' try: - newdata = b'' dlist = [self._io.fileno()] rlist, wlist, xlist = select(dlist, dlist, dlist, 1) if dlist[0] in rlist + wlist: @@ -114,7 +116,7 @@ class TCPConnection(object): try: return self._readbuffer.get(block=True, timeout=1) except queue.Empty: - continue + pass if not block: i -= 1 @@ -244,7 +246,7 @@ class Client(object): self.secop_id = line continue msgtype, spec, data = self.decode_message(line) - if msgtype in ('event', 'update', 'changed'): + if msgtype in (EVENTREPLY, READREPLY, WRITEREPLY): # handle async stuff self._handle_event(spec, data) # handle sync stuff @@ -252,23 +254,23 @@ class Client(object): def _handle_sync_reply(self, msgtype, spec, data): # handle sync stuff - if msgtype == "error": + if msgtype.startswith(ERRORPREFIX): # find originating msgtype and map to expected_reply_type # errormessages carry to offending request as the first # result in the resultist - _msgtype, _spec, _data = self.decode_message(data[0]) - _reply = self._get_reply_from_request(_msgtype) + request = msgtype[len(ERRORPREFIX):] + reply = REQUEST2REPLY.get(request, request) - entry = self.expected_replies.get((_reply, _spec), None) + entry = self.expected_replies.get((reply, spec), None) if entry: self.log.error("request %r resulted in Error %r" % - (data[0], spec)) - entry.extend([True, EXCEPTIONS[spec](*data)]) + ("%s %s" % (request, spec), (data[0], data[1]))) + entry.extend([True, EXCEPTIONS[data[0]](*data[1:])]) entry[0].set() return - self.log.error("got an unexpected error %s %r" % (spec, data[0])) + self.log.error("got an unexpected %s %r" % (msgtype,data[0:1])) return - if msgtype == "describing": + if msgtype == DESCRIPTIONREPLY: entry = self.expected_replies.get((msgtype, ''), None) else: entry = self.expected_replies.get((msgtype, spec), None) @@ -291,7 +293,7 @@ class Client(object): return req def decode_message(self, msg): - """return a decoded message tripel""" + """return a decoded message triple""" msg = msg.strip() if ' ' not in msg: return msg, '', None @@ -359,7 +361,7 @@ class Client(object): raise RuntimeError('Error decoding substruct of descriptive data: %r\n%r' % (err, data)) def _issueDescribe(self): - _, _, describing_data = self._communicate('describe') + _, _, describing_data = self._communicate(DESCRIPTIONREQUEST) try: describing_data = self._decode_substruct( ['modules'], describing_data) @@ -368,12 +370,6 @@ class Client(object): ['accessibles'], module) self.describing_data = describing_data -# import pprint -# def r(stuff): -# if isinstance(stuff, dict): -# return dict((k,r(v)) for k,v in stuff.items()) -# return stuff -# pprint.pprint(r(describing_data)) for module, moduleData in self.describing_data['modules'].items(): for aname, adata in moduleData['accessibles'].items(): @@ -403,21 +399,6 @@ class Client(object): def register_shutdown_callback(self, func, arg): self.connection.callbacks.append((func, arg)) - def _get_reply_from_request(self, requesttype): - # maps each (sync) request to the corresponding reply - # XXX: should go to the encoder! and be imported here - REPLYMAP = { # pylint: disable=C0103 - "describe": "describing", - "do": "done", - "change": "changed", - "activate": "active", - "deactivate": "inactive", - "read": "update", - #"*IDN?": "SECoP,", # XXX: !!! - "ping": "pong", - } - return REPLYMAP.get(requesttype, requesttype) - def communicate(self, msgtype, spec='', data=None): # only return the data portion.... return self._communicate(msgtype, spec, data)[2] @@ -426,15 +407,17 @@ class Client(object): self.log.debug('communicate: %r %r %r' % (msgtype, spec, data)) if self.stopflag: raise RuntimeError('alreading stopping!') - if msgtype == "*IDN?": + if msgtype == IDENTREQUEST: return self.secop_id # sanitize input msgtype = str(msgtype) spec = str(spec) - if msgtype not in ('*IDN?', 'describe', 'activate', 'deactivate', 'do', - 'change', 'read', 'ping', 'help'): + if msgtype not in (DESCRIPTIONREQUEST, ENABLEEVENTSREQUEST, + DISABLEEVENTSREQUEST, COMMANDREQUEST, + WRITEREQUEST, BUFFERREQUEST, + READREQUEST, HEARTBEATREQUEST, HELPREQUEST): raise EXCEPTIONS['Protocol'](args=[ self.encode_message(msgtype, spec, data), dict( @@ -443,13 +426,13 @@ class Client(object): ]) # handle syntactic sugar - if msgtype == 'change' and ':' not in spec: + if msgtype == WRITEREQUEST and ':' not in spec: spec = spec + ':target' - if msgtype == 'read' and ':' not in spec: + if msgtype == READREQUEST and ':' not in spec: spec = spec + ':value' # check if such a request is already out - rply = self._get_reply_from_request(msgtype) + rply = REQUEST2REPLY[msgtype] if (rply, spec) in self.expected_replies: raise RuntimeError( "can not have more than one requests of the same type at the same time!" @@ -487,7 +470,7 @@ class Client(object): def quit(self): # after calling this the client is dysfunctional! - self.communicate('deactivate') + self.communicate(DISABLEEVENTSREQUEST) self.stopflag = True if self._thread and self._thread.is_alive(): self.thread.join(self._thread) @@ -495,10 +478,10 @@ class Client(object): def startup(self, _async=False): self._issueDescribe() # always fill our cache - self.communicate('activate') + self.communicate(ENABLEEVENTSREQUEST) # deactivate updates if not wanted if not _async: - self.communicate('deactivate') + self.communicate(DISABLEEVENTSREQUEST) def queryCache(self, module, parameter=None): result = self._cache.get(module, {}) @@ -509,7 +492,7 @@ class Client(object): return result def getParameter(self, module, parameter): - return self.communicate('read', '%s:%s' % (module, parameter)) + return self.communicate(READREQUEST, '%s:%s' % (module, parameter)) def setParameter(self, module, parameter, value): datatype = self._getDescribingParameterData(module, @@ -517,7 +500,7 @@ class Client(object): value = datatype.from_string(value) value = datatype.export_value(value) - self.communicate('change', '%s:%s' % (module, parameter), value) + self.communicate(WRITEREQUEST, '%s:%s' % (module, parameter), value) @property def describingData(self): @@ -559,7 +542,7 @@ class Client(object): def execCommand(self, module, command, args): # ignore reply message + reply specifier, only return data - return self._communicate('do', '%s:%s' % (module, command), list(args) if args else None)[2] + return self._communicate(COMMANDREQUEST, '%s:%s' % (module, command), list(args) if args else None)[2] def getProperties(self, module, parameter): return self.describing_data['modules'][module]['accessibles'][parameter] @@ -574,4 +557,4 @@ class Client(object): def ping(self, pingctr=[0]): # pylint: disable=W0102 pingctr[0] = pingctr[0] + 1 - self.communicate("ping", pingctr[0]) + self.communicate(HEARTBEATREQUEST, pingctr[0]) diff --git a/secop/metaclass.py b/secop/metaclass.py index 07e5066..d3a5892 100644 --- a/secop/metaclass.py +++ b/secop/metaclass.py @@ -121,9 +121,10 @@ class ModuleMeta(type): if isinstance(v.datatype, EnumType) and not v.datatype._enum.name: v.datatype._enum.name = k - # newtype.accessibles will be used in 2 places only: + # newtype.accessibles will be used in 3 places only: # 1) for inheritance (see above) # 2) for the describing message + # 3) by code needing to access the Parameter/Command object (i.e. checking datatypes) newtype.accessibles = OrderedDict(sorted(accessibles.items(), key=lambda item: item[1].ctr)) # check validity of Parameter entries @@ -143,7 +144,12 @@ class ModuleMeta(type): 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) + try: + value = rfunc(self, maxage) + except Exception as e: + pobj = self.accessibles[pname] + self.DISPATCHER.announce_update_error(self, pname, pobj, e) + raise e else: # return cached value self.log.debug("rfunc(%s): return cached value" % pname) @@ -170,7 +176,11 @@ class ModuleMeta(type): value = pobj.datatype.validate(value) if wfunc: self.log.debug('calling %r(%r)' % (wfunc, value)) - returned_value = wfunc(self, value) + try: + returned_value = wfunc(self, value) + except Exception as e: + self.DISPATCHER.announce_update_error(self, pname, pobj, e) + raise e if returned_value is not None: value = returned_value # XXX: use setattr or direct manipulation diff --git a/secop/modules.py b/secop/modules.py index 00ec523..ba2eb75 100644 --- a/secop/modules.py +++ b/secop/modules.py @@ -26,14 +26,14 @@ from __future__ import division, print_function import sys import time -from secop.datatypes import EnumType, FloatRange, StringType, TupleOf, \ - get_datatype +from secop.datatypes import EnumType, FloatRange, \ + StringType, TupleOf, get_datatype from secop.errors import ConfigError, ProgrammingError -from secop.lib import formatException, formatExtendedStack, mkthread, \ - unset_value +from secop.lib import formatException, \ + formatExtendedStack, mkthread, unset_value from secop.lib.enum import Enum from secop.metaclass import ModuleMeta, add_metaclass -from secop.params import Command, Override, Parameter, PREDEFINED_ACCESSIBLES +from secop.params import PREDEFINED_ACCESSIBLES, Command, Override, Parameter # XXX: connect with 'protocol'-Modules. # Idea: every Module defined herein is also a 'protocol'-Module, diff --git a/secop/params.py b/secop/params.py index e3e8ddd..d91eddf 100644 --- a/secop/params.py +++ b/secop/params.py @@ -27,8 +27,6 @@ from secop.datatypes import CommandType, DataType from secop.errors import ProgrammingError from secop.lib import unset_value -EVENT_ONLY_ON_CHANGED_VALUES = False - class CountedObj(object): ctr = [0] @@ -193,6 +191,9 @@ class Override(CountedObj): if isinstance(obj, Accessible): props = obj.__dict__.copy() for key in self.kwds: + if key == 'unit': + # XXX: HACK! + continue if key not in props and key not in type(obj).valid_properties: raise ProgrammingError( "%s is not a valid %s property" % (key, type(obj).__name__)) diff --git a/secop/protocol/dispatcher.py b/secop/protocol/dispatcher.py index da47271..1250843 100644 --- a/secop/protocol/dispatcher.py +++ b/secop/protocol/dispatcher.py @@ -42,15 +42,15 @@ import threading from time import time as currenttime from secop.errors import SECoPServerError as InternalError -from secop.errors import BadValueError, NoSuchCommandError, \ - NoSuchModuleError, NoSuchParameterError, ProtocolError, ReadOnlyError +from secop.errors import BadValueError, NoSuchCommandError, NoSuchModuleError, \ + NoSuchParameterError, ProtocolError, ReadOnlyError, SECoPError from secop.params import Parameter from secop.protocol.messages import COMMANDREPLY, DESCRIPTIONREPLY, \ - DISABLEEVENTSREPLY, ENABLEEVENTSREPLY, EVENTREPLY, \ - HEARTBEATREPLY, IDENTREPLY, IDENTREQUEST, WRITEREPLY + DISABLEEVENTSREPLY, ENABLEEVENTSREPLY, ERRORPREFIX, EVENTREPLY, \ + HEARTBEATREPLY, IDENTREPLY, IDENTREQUEST, READREPLY, WRITEREPLY try: - unicode('a') + unicode except NameError: # no unicode on py3 unicode = str # pylint: disable=redefined-builtin @@ -104,6 +104,19 @@ class Dispatcher(object): [pobj.export_value(), dict(t=pobj.timestamp)]) self.broadcast_event(msg) + def announce_update_error(self, moduleobj, pname, pobj, err): + """called by modules param setters/getters to notify subscribers + + of problems + """ + # argument pname is no longer used here - should we remove it? + if not isinstance(err, SECoPError): + err = InternalError(err) + msg = (ERRORPREFIX + EVENTREPLY, u'%s:%s' % (moduleobj.name, pobj.export), + # error-report ! + [err.name, repr(err), dict(t=currenttime())]) + self.broadcast_event(msg) + def subscribe(self, conn, eventname): self._subscriptions.setdefault(eventname, set()).add(conn) @@ -140,7 +153,7 @@ class Dispatcher(object): return self._modules[modulename] elif modulename in list(self._modules.values()): return modulename - raise NoSuchModuleError('Module does not exist on this SEC-Node!') + raise NoSuchModuleError(u'Module does not exist on this SEC-Node!') def remove_module(self, modulename_or_obj): moduleobj = self.get_module(modulename_or_obj) @@ -188,18 +201,18 @@ class Dispatcher(object): result[u'modules'].append([modulename, mod_desc]) result[u'equipment_id'] = self.equipment_id result[u'firmware'] = u'FRAPPY - The Python Framework for SECoP' - result[u'version'] = u'2018.09' + result[u'version'] = u'2019.03' result.update(self.nodeprops) return result def _execute_command(self, modulename, command, argument=None): moduleobj = self.get_module(modulename) if moduleobj is None: - raise NoSuchModuleError('Module does not exist on this SEC-Node!') + raise NoSuchModuleError(u'Module does not exist on this SEC-Node!') cmdspec = moduleobj.accessibles.get(command, None) if cmdspec is None: - raise NoSuchCommandError('Module has no such command!') + raise NoSuchCommandError(u'Module has no such command!') if argument is None and cmdspec.datatype.argtype is not None: raise BadValueError(u'Command needs an argument!') @@ -216,16 +229,16 @@ class Dispatcher(object): def _setParameterValue(self, modulename, exportedname, value): moduleobj = self.get_module(modulename) if moduleobj is None: - raise NoSuchModuleError('Module does not exist on this SEC-Node!') + raise NoSuchModuleError(u'Module does not exist on this SEC-Node!') pname = moduleobj.accessiblename2attr.get(exportedname, None) pobj = moduleobj.accessibles.get(pname, None) if pobj is None or not isinstance(pobj, Parameter): - raise NoSuchParameterError('Module has no such parameter on this SEC-Node!') + raise NoSuchParameterError(u'Module has no such parameter on this SEC-Node!') if pobj.constant is not None: - raise ReadOnlyError('This parameter is constant and can not be accessed remotely.') + raise ReadOnlyError(u'This parameter is constant and can not be accessed remotely.') if pobj.readonly: - raise ReadOnlyError('This parameter can not be changed remotely.') + raise ReadOnlyError(u'This parameter can not be changed remotely.') writefunc = getattr(moduleobj, u'write_%s' % pname, None) # note: exceptions are handled in handle_request, not here! @@ -240,14 +253,14 @@ class Dispatcher(object): def _getParameterValue(self, modulename, exportedname): moduleobj = self.get_module(modulename) if moduleobj is None: - raise NoSuchModuleError('Module does not exist on this SEC-Node!') + raise NoSuchModuleError(u'Module does not exist on this SEC-Node!') pname = moduleobj.accessiblename2attr.get(exportedname, None) pobj = moduleobj.accessibles.get(pname, None) if pobj is None or not isinstance(pobj, Parameter): - raise NoSuchParameterError('Module has no such parameter on this SEC-Node!') + raise NoSuchParameterError(u'Module has no such parameter on this SEC-Node!') if pobj.constant is not None: - raise ReadOnlyError('This parameter is constant and can not be accessed remotely.') + raise ReadOnlyError(u'This parameter is constant and can not be accessed remotely.') readfunc = getattr(moduleobj, u'read_%s' % pname, None) if readfunc: @@ -297,12 +310,12 @@ class Dispatcher(object): def handle_read(self, conn, specifier, data): if data: - raise ProtocolError('poll request don\'t take data!') + raise ProtocolError('read requests don\'t take data!') modulename, pname = specifier, u'value' if ':' in specifier: modulename, pname = specifier.split(':', 1) # XXX: trigger polling and force sending event ??? - return (EVENTREPLY, specifier, list(self._getParameterValue(modulename, pname))) + return (READREPLY, specifier, list(self._getParameterValue(modulename, pname))) def handle_change(self, conn, specifier, data): modulename, pname = specifier, u'value' @@ -318,12 +331,12 @@ class Dispatcher(object): def handle_ping(self, conn, specifier, data): if data: - raise ProtocolError('poll request don\'t take data!') + raise ProtocolError('ping requests don\'t take data!') return (HEARTBEATREPLY, specifier, [None, {u't':currenttime()}]) def handle_activate(self, conn, specifier, data): if data: - raise ProtocolError('activate request don\'t take data!') + raise ProtocolError('activate requests don\'t take data!') if specifier: modulename, exportedname = specifier, None if ':' in specifier: @@ -368,6 +381,8 @@ class Dispatcher(object): return (ENABLEEVENTSREPLY, specifier, None) if specifier else (ENABLEEVENTSREPLY, None, None) def handle_deactivate(self, conn, specifier, data): + if data: + raise ProtocolError('deactivate requests don\'t take data!') if specifier: self.unsubscribe(conn, specifier) else: diff --git a/secop/protocol/interface/tcp.py b/secop/protocol/interface/tcp.py index e335a7f..2f408c0 100644 --- a/secop/protocol/interface/tcp.py +++ b/secop/protocol/interface/tcp.py @@ -29,7 +29,8 @@ from secop.errors import SECoPError from secop.lib import formatException, \ formatExtendedStack, formatExtendedTraceback from secop.protocol.interface import decode_msg, encode_msg_frame, get_msg -from secop.protocol.messages import HELPREPLY, HELPREQUEST, HelpMessage +from secop.protocol.messages import ERRORPREFIX, \ + HELPREPLY, HELPREQUEST, HelpMessage try: import socketserver # py3 @@ -113,29 +114,23 @@ class TCPRequestHandler(socketserver.BaseRequestHandler): if origin is None: break # no more messages to process origin = origin.strip() - if origin and origin[0] == CR: - origin = origin[1:] - if origin and origin[-1] == CR: - origin = origin[:-1] if origin in (HELPREQUEST, ''): # empty string -> send help message for idx, line in enumerate(HelpMessage.splitlines()): self.queue_async_reply((HELPREPLY, '%d' % (idx+1), line)) continue - msg = decode_msg(origin) result = None try: + msg = decode_msg(origin) result = serverobj.dispatcher.handle_request(self, msg) - if (msg[0] == 'read') and result: - # read should only trigger async_replies - self.queue_async_reply(('error', 'InternalError', [origin, - 'read should only trigger async data units'])) except SECoPError as err: - result = ('error', err.name, [origin, str(err), {'exception': formatException(), - 'traceback': formatExtendedStack()}]) + result = (ERRORPREFIX + msg[0], msg[1], [err.name, str(err), + {'exception': formatException(), + 'traceback': formatExtendedStack()}]) except Exception as err: # create Error Obj instead - result = ('error', 'InternalError', [origin, str(err), {'exception': formatException(), - 'traceback': formatExtendedStack()}]) + result = (ERRORPREFIX + msg[0], msg[1], ['InternalError', str(err), + {'exception': formatException(), + 'traceback': formatExtendedStack()}]) print('--------------------') print(formatException()) print('--------------------') diff --git a/secop/protocol/messages.py b/secop/protocol/messages.py index 2d69b81..fd27bf9 100644 --- a/secop/protocol/messages.py +++ b/secop/protocol/messages.py @@ -26,7 +26,7 @@ from __future__ import division, print_function IDENTREQUEST = u'*IDN?' # literal # literal! first part is fixed! -IDENTREPLY = u'SINE2020&ISSE,SECoP,V2018-11-07,v1.0\\beta' +IDENTREPLY = u'SINE2020&ISSE,SECoP,V2019-03-20,v1.0 RC1' DESCRIPTIONREQUEST = u'describe' # literal DESCRIPTIONREPLY = u'describing' # + +json @@ -52,13 +52,15 @@ BUFFERREQUEST = u'buffer' BUFFERREPLY = u'buffered' # +module[:parameter] -> NO direct reply, calls POLL internally! -POLLREQUEST = u'read' +READREQUEST = u'read' +READREPLY = u'reply' # See Issue 54 + EVENTREPLY = u'update' # +module[:parameter] +json_value (value, qualifiers_as_dict) HEARTBEATREQUEST = u'ping' # +nonce_without_space HEARTBEATREPLY = u'pong' # +nonce_without_space -ERRORREPLY = u'error' # +errorclass +json_extended_info +ERRORPREFIX = u'error_' # + specifier + json_extended_info(error_report) HELPREQUEST = u'help' # literal HELPREPLY = u'helping' # +line number +json_text @@ -72,7 +74,7 @@ REQUEST2REPLY = { COMMANDREQUEST: COMMANDREPLY, WRITEREQUEST: WRITEREPLY, BUFFERREQUEST: BUFFERREPLY, - POLLREQUEST: EVENTREPLY, + READREQUEST: READREPLY, HEARTBEATREQUEST: HEARTBEATREPLY, HELPREQUEST: HELPREPLY, } @@ -88,6 +90,6 @@ HelpMessage = u"""Try one of the following: '%s ' to request a heartbeat response '%s' to activate async updates '%s' to deactivate updates - """ % (IDENTREQUEST, DESCRIPTIONREQUEST, POLLREQUEST, + """ % (IDENTREQUEST, DESCRIPTIONREQUEST, READREQUEST, WRITEREQUEST, COMMANDREQUEST, HEARTBEATREQUEST, ENABLEEVENTSREQUEST, DISABLEEVENTSREQUEST) diff --git a/secop_demo/cryo.py b/secop_demo/cryo.py index 4c6e73b..beb090d 100644 --- a/secop_demo/cryo.py +++ b/secop_demo/cryo.py @@ -28,7 +28,7 @@ from math import atan from secop.datatypes import EnumType, FloatRange, TupleOf from secop.lib import clamp, mkthread -from secop.modules import Drivable, Parameter, Command, Override +from secop.modules import Command, Drivable, Override, Parameter # test custom property (value.test can be changed in config file) Parameter.add_property('test') diff --git a/test/test_datatypes.py b/test/test_datatypes.py index ac9aacf..247ebb5 100644 --- a/test/test_datatypes.py +++ b/test/test_datatypes.py @@ -28,7 +28,7 @@ import pytest from secop.datatypes import ArrayOf, BLOBType, BoolType, \ DataType, EnumType, FloatRange, IntRange, ProgrammingError, \ - StringType, StructOf, TupleOf, get_datatype, ScaledInteger + ScaledInteger, StringType, StructOf, TupleOf, get_datatype def test_DataType():