From 8e3d0da5dd8bc6a14743767c937c91fa4d1e7f55 Mon Sep 17 00:00:00 2001 From: Enrico Faulhaber Date: Thu, 19 Jan 2017 16:02:24 +0100 Subject: [PATCH] Polishing - unify (encoded) Errormessages. They now always contain the offeding request. - adopt keywords to current spec - fixed formatting of timestamp in WriteReply - minor bugfixing + polishing Change-Id: I0ab8836756551399643bdce3d062eedd345509f1 --- doc/SECoP_Messages.md | 6 +-- secop/client/baseclient.py | 14 +++--- secop/devices/core.py | 3 +- secop/protocol/dispatcher.py | 32 +++++++------- secop/protocol/encoding/demo_v4.py | 32 +++++++++----- secop/protocol/interface/tcp.py | 2 +- secop/protocol/messages.py | 69 +++++++++++++++++++----------- 7 files changed, 95 insertions(+), 63 deletions(-) diff --git a/doc/SECoP_Messages.md b/doc/SECoP_Messages.md index 7550f81..666b18c 100644 --- a/doc/SECoP_Messages.md +++ b/doc/SECoP_Messages.md @@ -146,9 +146,9 @@ Events can be emitted any time from the SEC-node (except if they would interrupt examples: - * 'event T1:value [3.479, {"t":"149128925.914882", "e":0.01924}] - * 'event T1:p [12, {"t":"149128927.193725"}]' - * 'event Vector:value [[0.01, 12.49, 3.92], {"t":"149128925.914882"}]' + * 'update T1:value [3.479, {"t":"149128925.914882", "e":0.01924}] + * 'update T1:p [12, {"t":"149128927.193725"}]' + * 'update Vector:value [[0.01, 12.49, 3.92], {"t":"149128925.914882"}]' ERROR diff --git a/secop/client/baseclient.py b/secop/client/baseclient.py index c2ad72d..3180be6 100644 --- a/secop/client/baseclient.py +++ b/secop/client/baseclient.py @@ -189,28 +189,30 @@ class Client(object): while not self.stopflag: line = self.connection.readline() self.log.debug('got answer %r' % line) - if line.startswith(('SECoP', 'Sine2020WP7')): + if line.startswith(('SECoP', 'SINE2020&ISSE,SECoP')): self.log.info('connected to: ' + line.strip()) self.secop_id = line continue msgtype, spec, data = self._decode_message(line) - if msgtype in ('event', 'changed'): + if msgtype in ('update', 'changed'): # handle async stuff self._handle_event(spec, data) - if msgtype != 'event': + if msgtype != 'update': # handle sync stuff if msgtype in self.expected_replies: entry = self.expected_replies[msgtype] entry.extend([msgtype, spec, data]) # wake up calling process entry[0].set() - elif msgtype == "ERROR": + elif msgtype == "error": # XXX: hack! if len(self.expected_replies) == 1: entry = self.expected_replies.values()[0] entry.extend([msgtype, spec, data]) # wake up calling process entry[0].set() + else: # try to find the right request.... + print data[0] # should be the origin request # XXX: make an assignment of ERROR to an expected reply. self.log.error('TODO: handle ERROR replies!') else: @@ -312,12 +314,12 @@ class Client(object): } if self.stopflag: raise RuntimeError('alreading stopping!') - if msgtype == 'poll': + if msgtype == 'read': # send a poll request and then check incoming events if ':' not in spec: spec = spec + ':value' event = threading.Event() - result = ['polled', spec] + result = ['update', spec] self.single_shots.setdefault(spec, set()).add( lambda d: (result.append(d), event.set())) self.connection.writeline( diff --git a/secop/devices/core.py b/secop/devices/core.py index 7e89621..f475761 100644 --- a/secop/devices/core.py +++ b/secop/devices/core.py @@ -32,6 +32,7 @@ import types import inspect import threading +from secop.lib.parsing import format_time from secop.errors import ConfigError, ProgrammingError from secop.protocol import status from secop.validators import enum, vector, floatrange @@ -73,7 +74,7 @@ class PARAM(object): unit=self.unit, readonly=self.readonly, value=self.value, - timestamp=self.timestamp, + timestamp=format_time(self.timestamp) if self.timestamp else None, validator=str(self.validator) if not isinstance( self.validator, type) else self.validator.__name__ ) diff --git a/secop/protocol/dispatcher.py b/secop/protocol/dispatcher.py index 56c076d..c10595a 100644 --- a/secop/protocol/dispatcher.py +++ b/secop/protocol/dispatcher.py @@ -43,7 +43,7 @@ import threading from messages import * from errors import * - +from secop.lib.parsing import format_time class Dispatcher(object): @@ -84,16 +84,15 @@ class Dispatcher(object): reply = handler(conn, msg) except SECOPError as err: self.log.exception(err) - reply = ErrorMessage(errorclass=err.__class__.__name__, - errorinfo=[repr(err), str(msg)]) + reply = msg.get_error(errorclass=err.__class__.__name__, + errorinfo=[repr(err), str(msg)]) except (ValueError, TypeError) as err: - # self.log.exception(err) - reply = ErrorMessage(errorclass='BadValue', - errorinfo=[repr(err), str(msg)]) + reply = msg.get_error(errorclass='BadValue', + errorinfo=[repr(err), str(msg)]) except Exception as err: self.log.exception(err) - reply = ErrorMessage(errorclass='InternalError', - errorinfo=[repr(err), str(msg)]) + reply = msg.get_error(errorclass='InternalError', + errorinfo=[repr(err), str(msg)]) else: self.log.debug('Can not handle msg %r' % msg) reply = self.unhandled(conn, msg) @@ -159,8 +158,11 @@ class Dispatcher(object): self._export.append(modulename) def get_module(self, modulename): - module = self._modules.get(modulename, modulename) - return module + if modulename in self._modules: + return self._modules[modulename] + elif modulename in self._modules.values(): + return modulename + raise NoSuchModuleError(module=str(modulename)) def remove_module(self, modulename_or_obj): moduleobj = self.get_module(modulename_or_obj) or modulename_or_obj @@ -226,7 +228,7 @@ class Dispatcher(object): moduleobj = self.get_module(modulename) if moduleobj is None: - raise NoSuchmoduleError(module=modulename) + raise NoSuchModuleError(module=modulename) cmdspec = moduleobj.CMDS.get(command, None) if cmdspec is None: @@ -249,7 +251,7 @@ class Dispatcher(object): def _setParamValue(self, modulename, pname, value): moduleobj = self.get_module(modulename) if moduleobj is None: - raise NoSuchmoduleError(module=modulename) + raise NoSuchModuleError(module=modulename) pobj = moduleobj.PARAMS.get(pname, None) if pobj is None: @@ -267,7 +269,7 @@ class Dispatcher(object): return WriteReply( module=modulename, parameter=pname, value=[ pobj.value, dict( - t=pobj.timestamp)]) + t=format_time(pobj.timestamp))]) return WriteReply( module=modulename, parameter=pname, @@ -389,5 +391,5 @@ class Dispatcher(object): (no handle_ method was defined) """ self.log.error('IGN: got unhandled request %s' % msg) - return ErrorMessage(errorclass="InternalError", - errorstring='Got Unhandled Request %r' % msg) + return msg.get_error(errorclass="InternalError", + errorinfo="Unhandled Request") diff --git a/secop/protocol/encoding/demo_v4.py b/secop/protocol/encoding/demo_v4.py index 49a80d7..3177350 100644 --- a/secop/protocol/encoding/demo_v4.py +++ b/secop/protocol/encoding/demo_v4.py @@ -45,8 +45,9 @@ DEMO_RE = re.compile( #""" # messagetypes: IDENTREQUEST = '*IDN?' # literal -# literal! first part 'SECoP' is fixed! -IDENTREPLY = 'SECoP, SECoPTCP, V2016-11-30, rc1' +# literal! first part is fixed! +#IDENTREPLY = 'SECoP, SECoPTCP, V2016-11-30, rc1' +IDENTREPLY = 'SINE2020&ISSE,SECoP,V2016-11-30,rc1' DESCRIPTIONSREQUEST = 'describe' # literal DESCRIPTIONREPLY = 'describing' # + +json ENABLEEVENTSREQUEST = 'activate' # literal @@ -61,11 +62,11 @@ WRITEREQUEST = 'change' # +module[:parameter] +json_value # send with the read back value WRITEREPLY = 'changed' # +module[:parameter] -> NO direct reply, calls TRIGGER internally! -TRIGGERREQUEST = 'poll' -EVENT = 'event' # +module[:parameter] +json_value (value, qualifiers_as_dict) +TRIGGERREQUEST = 'read' +EVENT = 'update' # +module[:parameter] +json_value (value, qualifiers_as_dict) HEARTBEATREQUEST = 'ping' # +nonce_without_space HEARTBEATREPLY = 'pong' # +nonce_without_space -ERRORREPLY = 'ERROR' # +errorclass +json_extended_info +ERRORREPLY = 'error' # +errorclass +json_extended_info HELPREQUEST = 'help' # literal HELPREPLY = 'helping' # +line number +json_text ERRORCLASSES = ['NoSuchDevice', 'NoSuchParameter', 'NoSuchCommand', @@ -88,6 +89,10 @@ def encode_value_data(vobj): q['t'] = format_time(q['t']) return vobj.value, q +def encode_error_msg(emsg): + # note: result is JSON-ified.... + return [emsg.origin, dict( (k,getattr(emsg, k)) for k in emsg.ARGS if k != 'origin')] + class DemoEncoder(MessageEncoder): # map of msg to msgtype string as defined above. @@ -103,12 +108,12 @@ class DemoEncoder(MessageEncoder): CommandRequest: (COMMANDREQUEST, lambda msg: "%s:%s" % (msg.module, msg.command), 'arguments',), CommandReply: (COMMANDREPLY, lambda msg: "%s:%s" % (msg.module, msg.command), encode_cmd_result,), WriteRequest: (WRITEREQUEST, lambda msg: "%s:%s" % (msg.module, msg.parameter) if msg.parameter else msg.module, 'value',), - WriteReply: (WRITEREPLY, lambda msg: "%s:%s" % (msg.module, msg.parameter) if msg.parameter else msg.module, 'value',), + WriteReply: (WRITEREPLY, lambda msg: "%s:%s" % (msg.module, msg.parameter) if msg.parameter else msg.module, 'value', ), PollRequest: (TRIGGERREQUEST, lambda msg: "%s:%s" % (msg.module, msg.parameter) if msg.parameter else msg.module, ), HeartbeatRequest: (HEARTBEATREQUEST, 'nonce',), HeartbeatReply: (HEARTBEATREPLY, 'nonce',), HelpMessage: (HELPREQUEST, ), - ErrorMessage: (ERRORREPLY, 'errorclass', 'errorinfo',), + ErrorMessage: (ERRORREPLY, "errorclass", encode_error_msg,), Value: (EVENT, lambda msg: "%s:%s" % (msg.module, msg.parameter or (msg.command + '()')) if msg.parameter or msg.command else msg.module, encode_value_data,), @@ -185,7 +190,8 @@ class DemoEncoder(MessageEncoder): if msgspec is None and data: return ErrorMessage(errorclass='InternalError', errorinfo='Regex matched json, but not spec!', - is_request=True) + is_request=True, + origin=encoded) if msgtype in self.DECODEMAP: if msgspec and ':' in msgspec: @@ -197,13 +203,17 @@ class DemoEncoder(MessageEncoder): data = json.loads(data) except ValueError as err: return ErrorMessage(errorclass='BadValue', - errorinfo=[repr(err), str(encoded)]) - return self.DECODEMAP[msgtype](msgspec, data) + errorinfo=[repr(err), str(encoded)], + origin=encoded) + msg = self.DECODEMAP[msgtype](msgspec, data) + msg.setvalue("origin",encoded) + return msg return ErrorMessage( errorclass='SyntaxError', errorinfo='%r: No Such Messagetype defined!' % encoded, - is_request=True) + is_request=True, + origin=encoded) def tests(self): print "---- Testing encoding -----" diff --git a/secop/protocol/interface/tcp.py b/secop/protocol/interface/tcp.py index e9ccfbd..9afef2f 100644 --- a/secop/protocol/interface/tcp.py +++ b/secop/protocol/interface/tcp.py @@ -128,7 +128,7 @@ class TCPServer(SocketServer.ThreadingTCPServer): # and encoders (to en/decode messages from frames) self.framingCLS = FRAMERS[interfaceopts.pop('framing', 'none')] self.encodingCLS = ENCODERS[interfaceopts.pop('encoding', 'pickle')] - self.log.debug("TCPServer binding to %s:%d" % (bindto, portnum)) + self.log.info("TCPServer binding to %s:%d" % (bindto, portnum)) self.log.debug("TCPServer using framing=%s" % self.framingCLS.__name__) self.log.debug("TCPServer using encoding=%s" % self.encodingCLS.__name__) diff --git a/secop/protocol/messages.py b/secop/protocol/messages.py index 668e005..a89dbcc 100644 --- a/secop/protocol/messages.py +++ b/secop/protocol/messages.py @@ -29,6 +29,7 @@ class Message(object): is_reply = False is_error = False qualifiers = {} + origin = "" def __init__(self, **kwds): self.ARGS = set() @@ -68,11 +69,39 @@ class Value(object): devspec = '%s:%s()' % (devspec, self.command) return '%s:Value(%s)' % (devspec, ', '.join( [repr(self.value)] + - ['%s=%s' % (k, repr(v)) for k, v in self.qualifiers.items()])) + ['%s=%s' % (k, format_time(v) if k=="timestamp" else repr(v)) for k, v in self.qualifiers.items()])) -class IdentifyRequest(Message): +class Request(Message): is_request = True + def get_reply(self): + """returns a Reply object prefilled with the attributes from this request.""" + m = Message() + m.is_request = False + m.is_reply = True + m.is_error = False + m.qualifiers = self.qualifiers + m.origin = self.origin + for k in self.ARGS: + m.setvalue(k, self.__dict__[k]) + return m + + def get_error(self, errorclass, errorinfo): + """returns a Reply object prefilled with the attributes from this request.""" + m = ErrorMessage() + m.qualifiers = self.qualifiers + m.origin = self.origin + for k in self.ARGS: + m.setvalue(k, self.__dict__[k]) + m.setvalue("errorclass", errorclass[:-5] + if errorclass.endswith('rror') + else errorclass) + m.setvalue("errorinfo", errorinfo) + return m + + +class IdentifyRequest(Request): + pass class IdentifyReply(Message): @@ -80,8 +109,8 @@ class IdentifyReply(Message): version_string = None -class DescribeRequest(Message): - is_request = True +class DescribeRequest(Request): + pass class DescribeReply(Message): @@ -90,24 +119,23 @@ class DescribeReply(Message): description = None -class ActivateRequest(Message): - is_request = True +class ActivateRequest(Request): + pass class ActivateReply(Message): is_reply = True -class DeactivateRequest(Message): - is_request = True +class DeactivateRequest(Request): + pass class DeactivateReply(Message): is_reply = True -class CommandRequest(Message): - is_request = True +class CommandRequest(Request): command = '' arguments = [] @@ -118,8 +146,7 @@ class CommandReply(Message): result = None -class WriteRequest(Message): - is_request = True +class WriteRequest(Request): module = None parameter = None value = None @@ -132,14 +159,13 @@ class WriteReply(Message): value = None -class PollRequest(Message): +class PollRequest(Request): is_request = True module = None parameter = None -class HeartbeatRequest(Message): - is_request = True +class HeartbeatRequest(Request): nonce = 'alive' @@ -163,16 +189,7 @@ class ErrorMessage(Message): errorinfo = None -class HelpMessage(Message): - is_reply = True - is_request = True +class HelpMessage(Request): + is_reply = True #!sic! -if __name__ == '__main__': - print("Minimal testing of messages....") - m = Message(MSGTYPE='test', a=1, b=2, c='x') - print m - print ReadMessage(devs=['a'], result=[Value(12.3)]) - - print "OK" - print