rework message syntax to conform to latest decisions

needs a bigger rework, since READREPLY and EVENTREPLY are now different....
Also the format of the error-reply got changed :(

Change-Id: I1760743238227730ee49aaf92b54e0ff5f25423b
Reviewed-on: https://forge.frm2.tum.de/review/20246
Tested-by: JenkinsCodeReview <bjoern_pedersen@frm2.tum.de>
Reviewed-by: Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
This commit is contained in:
Enrico Faulhaber
2019-03-27 15:29:19 +01:00
parent 94619723a9
commit 94959f2e9b
9 changed files with 107 additions and 101 deletions

View File

@ -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:
try:
newdata = b''
try:
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])

View File

@ -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))
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))
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

View File

@ -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,

View File

@ -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__))

View File

@ -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:

View File

@ -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,28 +114,22 @@ 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(),
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(),
result = (ERRORPREFIX + msg[0], msg[1], ['InternalError', str(err),
{'exception': formatException(),
'traceback': formatExtendedStack()}])
print('--------------------')
print(formatException())

View File

@ -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' # +<id> +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 <nonce>' 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)

View File

@ -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')

View File

@ -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():